核心网页指标到底在测什么
Core Web Vitals 从 2021 年成为 Google 的排名信号之后,被各种「优化服务」讲得很玄。但落实到个人站长的实际操作上,它其实只有三个数字:LCP(最大内容绘制)、INP(交互到下一次绘制)、CLS(累积布局偏移)。这三个数字分别对应「打开得快不快」「点得动不动」「看着跳不跳」。
这篇文章不讲概念定义,讲的是:怎么在自己那台 1 核 2G 的 VPS 上,用最少的改动把这三个数字压进绿色区间。所有方法都在真实的 Typecho / WordPress 小站上验证过,不依赖付费 CDN 或者前端框架重构。
一、先建立可信的测量基线
优化之前必须先量。这里最大的坑是:你在自己电脑上测出来的分数,和真实用户完全不是一回事。
真实用户的数据来自 Chrome UX Report(CrUX),28 天滚动窗口,按访问量加权。它包含三类设备(桌面、手机、平板)和不同网络条件。而 Lighthouse 实验室数据是模拟的、单次冷启动的、干净的。两者差异可能高达 40%。
所以分两条线走:
- 实验室数据(用来定位问题):PageSpeed Insights 或本地 Lighthouse,跑移动端,多跑三次取中位数。它告诉你「哪里慢」,但不代表全部用户。
- 现场数据(用来确认真实效果):Search Console 的「核心网页指标」报告,加上自己埋的 web-vitals.js。它告诉你「多少人受影响」。
自己埋点最简单的方式是用官方库,不依赖构建工具:
<script type="module">
import {onLCP, onINP, onCLS} from 'https://unpkg.com/web-vitals@4/dist/web-vitals.attribution.js';
function send(metric) {
navigator.sendBeacon('/vitals', JSON.stringify({
name: metric.name,
value: metric.value,
id: metric.id,
rating: metric.rating,
path: location.pathname,
nav: performance.getEntriesByType('navigation')[0]?.type
}));
}
onLCP(send); onINP(send); onCLS(send);
</script>服务端只需要收下这个 beacon 落到日志里。一周之后你就有按页面、按设备分组的真实分布,比任何第三方工具都准。注意 metric.attribution 版本的库体积略大,如果只想要数字可以用 web-vitals.js(不带 attribution)。
阈值标准(2026 年现行):
- LCP:好 ≤ 2.5s,需改进 2.5~4.0s,差 > 4.0s
- INP:好 ≤ 200ms,需改进 200~500ms,差 > 500ms
- CLS:好 ≤ 0.1,需改进 0.1~0.25,差 > 0.25
注意是第 75 百分位,不是平均值。也就是说你必须让 75% 的访问都在阈值内才算通过。这意味着长尾的慢请求(老手机、弱网)会严重拉低你的分数。
二、LCP 优化:找到那个「最大元素」
LCP 慢的原因只有四种,且必须按顺序排查,因为前一步不解决,后一步优化都是白费:
- 服务器响应慢(TTFB):LCP 不可能快于 TTFB。如果 TTFB 是 1.2 秒,你的 LCP 就不可能在 1.5 秒内。
- 资源加载延迟:图片/字体被 CSS 或 JS 阻塞。
- 元素渲染延迟:JS 阻塞主线程,导致元素迟迟画不出来。
- 元素本身太大:一张 2MB 的 hero 图,即使加载很快,解码也慢。
第 1 步:先量 TTFB。用 curl 直接测:
for i in 1 2 3 4 5; do
curl -s -o /dev/null -w "TTFB: %{time_starttransfer}s total: %{time_total}s\n" \
"https://www.example.com/"
done如果 TTFB 稳定超过 600ms,先别管前端,去看 PHP-FPM 和数据库。个人站 TTFB 慢的三大原因是:没开 OPcache、MySQL 慢查询、以及没做页面级缓存。其中页面缓存收益最大——一篇文章页如果用静态 HTML 直接返回,TTFB 能从 800ms 降到 30ms。
Nginx 的 fastcgi_cache 是最省事的方案,不装插件也能做:
# nginx.conf 的 http 块
fastcgi_cache_path /var/cache/nginx/fcgi levels=1:2 keys_zone=PHP:100m inactive=60m max_size=1g;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
fastcgi_cache_lock on;
fastcgi_cache_use_stale error timeout updating http_500 http_503;
fastcgi_cache_background_update on;
# server 块内
set $skip_cache 0;
if ($request_method = POST) { set $skip_cache 1; }
if ($query_string != "") { set $skip_cache 1; }
if ($request_uri ~* "/admin|/action|/install|sitemap") { set $skip_cache 1; }
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_cache PHP;
fastcgi_cache_valid 200 10m;
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
add_header X-Cache $upstream_cache_status;
}关键点是 add_header X-Cache,它让你能立刻验证缓存是否命中:
curl -sI "https://www.example.com/post-1.html" | grep -i x-cache
# 期望看到 X-Cache: HIT第 2、4 步:图片。这是个人站最容易拿分的地方。三个动作,按投入产出排序:
- 转 WebP/AVIF:同一张图,JPEG 200KB 转 WebP 通常 80KB,转 AVIF 50KB。用
cwebp或avifenc批量转,保留原图做 fallback。 - 显式写宽高:
<img width="800" height="450">,这不是为了 CLS,也是为了让浏览器提前分配空间,LCP 判定更早完成。 - 首屏图不要 lazy load:这是最常见的错误。给首屏 hero 图加
loading="lazy"会人为把 LCP 推迟到 1~2 秒后。首屏图必须是loading="eager"(默认)+fetchpriority="high"。
<img src="/img/hero-800.webp"
srcset="/img/hero-400.webp 400w, /img/hero-800.webp 800w, /img/hero-1200.webp 1200w"
sizes="(max-width: 600px) 100vw, 800px"
width="800" height="450"
fetchpriority="high"
decoding="sync"
alt="文章题图">第 3 步:JS 阻塞。个人站常见的是统计代码、广告代码、和几个 jQuery 插件。处理原则:
- 非关键的(统计、广告)加
defer或async。 - 第三方脚本可以用
requestIdleCallback延后加载。 - 如果某段 JS 只在一个页面上用,就只在那页引,别全局引。
检查方法是在 Chrome DevTools 的 Performance 面板录制一次加载,看主线程有没有超过 50ms 的长任务(Long Task)落在 LCP 之前。有的话点开看是谁。
三、INP 优化:让主线程空出手来
INP 取代了 FID,它衡量的是用户每一次交互的响应延迟,取最差的一次(准确说是高百分位)。对内容站来说,最常见的交互是点击菜单、切换标签、以及评论框输入。
INP 差的根因几乎总是同一个:主线程被长任务占满,用户的点击只能排队。
几个立竿见影的做法:
- 拆分长任务。一个大循环处理 5000 条数据,改成每 50 条
setTimeout(fn, 0)让出主线程。 - 事件处理器里不要做同步重活。点击后先给视觉反馈(改 class),再
requestAnimationFrame里做实际计算。 - 用事件委托而不是给每个元素绑监听。列表 500 项各绑一个 click,绑定本身就吃掉几十毫秒。
- 输入框防抖。搜索框每敲一个字就发请求 + 重渲染列表,是最典型的卡顿来源,加 300ms debounce。
- 第三方脚本隔离。广告和统计脚本尽量放 iframe,避免它们的 JS 跑在你的主线程上。
验证 INP 的办法:DevTools Performance 面板录制,然后在页面上点几下,看 Interactions 轨道上每条的 duration。超过 200ms 的就要查。
四、CLS 优化:把所有会跳的东西钉死
CLS 是最容易一次修好、永久受益的指标。它统计的是页面加载过程中,可见元素的非预期位置变化。常见的四个来源:
- 图片没写宽高 —— 图片加载完成后撑开空间,下方内容集体下移。修法:写
width/height属性,或者用 CSSaspect-ratio。 - 广告位没预留空间 —— 广告加载后插入,把正文推下去。修法:给广告容器写死
min-height。 - 字体替换导致重排 —— 自定义字体加载完成后文字宽度变化。修法:
font-display: optional(不换字体)或size-adjust调整 fallback 字体度量。 - 动态插入的内容在视口上方 —— 比如延迟加载的公告条。修法:要么预留空间,要么插入到视口下方。
广告位的正确写法:
<div class="ad-slot" style="min-height:280px;contain:layout">
<ins class="adsbygoogle" ...></ins>
</div>contain: layout 告诉浏览器这个容器的内部变化不影响外部布局,是防止广告抖动的一个额外保险。
字体的正确写法(用 size-adjust 让 fallback 字体度量接近真实字体):
@font-face {
font-family: 'MyFont';
src: url('/fonts/myfont.woff2') format('woff2');
font-display: swap;
size-adjust: 96%;
ascent-override: 92%;
descent-override: 22%;
}
@font-face {
font-family: 'MyFontFallback';
src: local('PingFang SC'), local('Microsoft YaHei');
size-adjust: 104%;
}对中文站来说,还有一个特有的大坑:中文字体文件动辄 5~10MB。全量加载一定会拖慢首屏。正确做法是子集化——只保留页面实际用到的字符。用 fonttools 的 pyftsubset:
pip install fonttools brotli
pyftsubset SourceHanSans.ttf \
--text-file=常用字.txt \
--flavor=woff2 \
--output-file=sourcehan-subset.woff2 \
--layout-features='*'一个 7000 常用汉字的子集能压到 1.5MB 左右,如果只做标题字体(几百字),能压到 200KB 以内。再配合 unicode-range 分片,按需加载。
五、那些「看起来有用但其实没用」的优化
市面上流传的很多做法,对 Core Web Vitals 几乎没有影响,白费功夫:
- 合并所有 CSS/JS 成一个大文件:HTTP/2 下多路复用已经解决了请求数问题,合并反而导致缓存粒度变粗、首字节内容要等整个包。现在更推荐按页面拆分。
- 全站 CDN 加速:如果你的用户 90% 在国内而服务器也在国内,CDN 对 TTFB 的提升很有限,回源配置错了还更慢。先测 TTFB 再决定。
- 追求 Lighthouse 满分:100 分不等于用户体验好。Search Console 的现场数据才是真相。
- 给所有图片加 lazy load:首屏图 lazy 会直接恶化 LCP,这是最常见的负面优化。
- 上 PWA / Service Worker 提速:对首次访问完全没帮助,只影响回访。个人站先别管。
六、一个真实的优化过程
拿一个 Typecho 站做例子,原始数据(移动端 CrUX,75 分位):LCP 4.3s、INP 210ms、CLS 0.18。全部不通过。
第一步,测 TTFB,发现文章页 1.1s。查 PHP-FPM 慢日志,发现是每页都在查同一个「最新文章」列表和「热门标签」,且没缓存。加 fastcgi_cache 之后 TTFB 降到 40ms。
第二步,重测 LCP,降到 2.8s。用 Performance 面板看最大元素是文章头图。检查发现那张图是 1.8MB 的 PNG,且带 loading="lazy"。转成 WebP(82KB)+ 去掉 lazy + 加 fetchpriority="high" 后,LCP 降到 1.6s。
第三步,CLS 0.18 的来源是广告位。给广告容器加 min-height:280px,CLS 降到 0.02。
第四步,INP 210ms。录制后发现点击「展开评论」时同步渲染了 200 条评论。改成先渲染 20 条 + 分页按钮,INP 降到 90ms。
整个过程只改了一个 Nginx 配置、一张图、一个 CSS 属性、一段 JS。没有重构,没有换主题,没有上付费服务。三周后 CrUX 更新,三项全绿。
七、日常巡检:别让分数悄悄退回去
Core Web Vitals 是会被「改回去」的:新装一个插件、加一个统计代码、换一张没压缩的图,分数就掉了。所以要做定期巡检。
方案一,用 PageSpeed Insights API 做定时任务,每天跑一次首页和三个热门页,把分数落库:
#!/bin/bash
KEY="你的API_KEY"
for u in "https://www.example.com/" "https://www.example.com/hot-1.html"; do
curl -s "https://www.googleapis.com/pagespeedonline/v5/runPagespeed?url=$u&strategy=mobile&key=$KEY" \
| jq -r --arg u "$u" '"\($u) LCP=\(.loadingExperience.metrics.LARGEST_CONTENTFUL_PAINT_MS.percentile)ms CLS=\(.loadingExperience.metrics.CUMULATIVE_LAYOUT_SHIFT_SCORE.percentile) INP=\(.loadingExperience.metrics.INTERACTION_TO_NEXT_PAINT.percentile)ms"' \
>> /var/log/cwv.log
done方案二,用自建的 web-vitals beacon 数据,每周统计一次 P75。这个更准,但要自己写聚合逻辑。
无论哪种,关键是用 cron 跑起来、数据落盘。分数一旦从 1.6s 涨到 2.9s,你能在一周内看到,而不是等到流量掉了才发现。
八、小结
把上面所有内容压成一张检查清单,按执行顺序排列:
- 测 TTFB,超过 600ms 先做页面缓存。
- 测 LCP 元素是谁,图片就转 WebP + 去 lazy + fetchpriority,脚本就 defer。
- 给所有图片写 width/height,给广告位写 min-height,给字体写 font-display。
- 录制一次交互,找超过 200ms 的长任务,拆分或延后。
- 埋 web-vitals beacon,每周看 P75 趋势。
这套流程在一台普通 VPS 上,通常能把移动端三项指标从「全红」拉到「全绿」,代价是两个小时的工作量。比起买任何优化服务,这是个人站长性价比最高的投入。