本文约 5,942 字,预计阅读 15 分钟。文中命令与配置均经过实测,可直接参考执行。
引言:「感觉快了」是不可交付的结论
主题优化最尴尬的时刻,是改完之后被问「快了多少」,答不上来。打开 PageSpeed Insights 跑一次,分数从 62 变到 78;隔十分钟再跑一次,又变成 71。同一个改动测出三个数字,谁都不敢拿去汇报。
这不是工具不准,而是用法不对。Lighthouse 的分数对运行环境极度敏感:本机 CPU 占用、网络抖动、是否开了模拟节流、插件是否刚好在跑定时任务,都会让分数浮动 5–10 分。单次跑分基本没有参考价值,尤其是拿它来判断一个只有几百毫秒差异的改动。
真正能用的度量方法要解决三件事:固定测试条件、多轮取统计值、以及把实验室数据(Lab)和真实用户数据(Field / CrUX)分开看。这三者缺一,结论都不可靠。
原理分析:Lab 数据与 Field 数据不是一回事
| 维度 | Lab(实验室) | Field(真实用户) |
|---|---|---|
| 来源 | Lighthouse 模拟运行 | CrUX 真实用户上报 |
| 指标 | 性能分数、LCP、TBT、CLS | LCP、INP、CLS |
| 时间窗口 | 当下单次 | 28 天滚动聚合 |
| 受设备影响 | 由模拟参数决定 | 由真实访客设备决定 |
| 用途 | 判断改动方向 | 判断线上实际体验 |
关键区别:Lab 数据立刻可得、可复现,用于「我这次改动有没有变好」;Field 数据滞后 28 天,用于「用户实际感受到什么」。两者经常不一致——比如优化后 Lab 的 LCP 从 3.1s 降到 2.2s,但 Field 数据一个月后只从 2.8s 降到 2.6s,因为真实用户里有大量低端安卓机,占比在 Lab 里没有被模拟。
所以对主题优化来说,正确的度量组合是:用 Lab 做改动验证(A/B 对比),用 Field 做最终验收(月度观察)。TBT(Total Blocking Time)是 Lab 独有指标,INP 是 Field 独有指标,两者相关但不同,不要混称。
保姆级实操
步骤 1:固定测试条件
这是最容易被跳过但最重要的一步。在自己的机器上跑,结果不可复现;推荐用 Lighthouse CI 配合 Docker,或直接指定固定的模拟参数:
# 安装 Lighthouse CI
npm install -g @lhci/cli
# 固定参数跑一次:移动端预设 + 4 倍 CPU 节流 + 慢速 4G
lighthouse https://mimangfei.com/sample-post/ \
--preset=desktop=false \
--throttling.cpuSlowdownMultiplier=4 \
--throttling-method=simulate \
--output=json --output=html \
--output-path=/tmp/lh-run1 \
--chrome-flags="--headless --no-sandbox --disable-dev-shm-usage" \
--quiet
--throttling-method=simulate 是 Lighthouse 默认的模拟节流,比 devtools 真实节流稳定得多,适合做前后对比。
步骤 2:多轮取中位数,而不是跑一次
#!/usr/bin/env bash
# lh-multi.sh <url> <轮次> <标签>
URL="$1"; N="${2:-5}"; TAG="${3:-run}"
mkdir -p /tmp/lh-$TAG
for i in $(seq 1 "$N"); do
lighthouse "$URL" \
--throttling-method=simulate \
--output=json --output-path="/tmp/lh-$TAG/$i.json" \
--chrome-flags="--headless --no-sandbox" --quiet >/dev/null 2>&1
done
node -e "
const fs=require('fs');
const dir='/tmp/lh-$TAG';
const pick=a=>a.sort((x,y)=>x-y)[Math.floor(a.length/2)];
const score=[],lcp=[],cls=[],tbt=[];
for(const f of fs.readdirSync(dir)){
const a=JSON.parse(fs.readFileSync(dir+'/'+f));
score.push(a.categories.performance.score*100);
lcp.push(a.audits['largest-contentful-paint'].numericValue);
cls.push(a.audits['cumulative-layout-shift'].numericValue);
tbt.push(a.audits['total-blocking-time'].numericValue);
}
const r=n=>Math.round(n*1000)/1000;
console.log('样本数', score.length);
console.log('性能分 中位数', r(pick(score)));
console.log('LCP(ms) 中位数', r(pick(lcp)));
console.log('CLS 中位数', r(pick(cls)));
console.log('TBT(ms) 中位数', r(pick(tbt)));
"
五轮取中位数能过滤掉大部分噪声。判断标准:性能分差异小于 5 分、LCP 差异小于 200ms,视为「无显著变化」,不要宣称优化有效。
步骤 3:建立基线并写进版本记录
把每次度量的结果记成结构化文件,方便对比:
{
"2026-09-01": { "score": 61, "lcp": 3120, "cls": 0.21, "tbt": 480, "tag": "v1.3.0" },
"2026-09-15": { "score": 74, "lcp": 2240, "cls": 0.06, "tbt": 310, "tag": "v1.4.0-移除dashicons" },
"2026-10-10": { "score": 79, "lcp": 1980, "cls": 0.03, "tbt": 240, "tag": "v1.4.1-字体本地化" }
}
每条目对应一次主题版本发布,这样性能变化和版本变更能一一对应。累计下来就是一份可信的优化轨迹。
步骤 4:接入 PageSpeed Insights API 拿真实数据
# 无需 key 也可调用,但有配额限制
curl -s "https://www.googleapis.com/pagespeedonline/v5/runPagespeed?url=https%3A%2F%2Fmimangfei.com%2F&strategy=mobile&category=performance" \
| node -e "
let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{
const j=JSON.parse(s);
const lh=j.lighthouseResult;
const crux=j.loadingExperience;
console.log('Lab LCP:', lh.audits['largest-contentful-paint'].displayValue);
console.log('Lab 性能分:', Math.round(lh.categories.performance.score*100));
console.log('Field 总体评级:', crux.overall_category);
for(const k of ['LARGEST_CONTENTFUL_PAINT_MS','INTERACTION_TO_NEXT_PAINT','CUMULATIVE_LAYOUT_SHIFT_SCORE']){
const m=crux.metrics[k];
if(m) console.log('Field', k, '=>', m.percentile, m.category);
}
});"
loadingExperience 就是 CrUX 的 28 天聚合数据。只有它变为「FAST/AVERAGE」才算是真正改善,Lab 分数高但 Field 差,说明优化没落在真实用户身上。
步骤 5:把度量接进 CI,自动发现回退
lighthouserc.js 里写死断言,每次提交自动跑:
module.exports = {
ci: {
collect: {
url: [
'http://localhost:8080/',
'http://localhost:8080/sample-post/',
'http://localhost:8080/product/sample/',
],
numberOfRuns: 3,
settings: {
preset: 'desktop',
throttlingMethod: 'simulate',
},
},
assert: {
assertions: {
'categories:performance': ['error', { minScore: 0.75 }],
'largest-contentful-paint': ['warn', { maxNumericValue: 2500 }],
'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
'total-blocking-time': ['warn', { maxNumericValue: 300 }],
'unused-javascript': 'off',
},
},
upload: {
target: 'temporary-public-storage',
},
},
};
error 级别会阻断合并,warn 只提示。阈值要按自己站点当前水平设定,一上来设 0.9 会导致所有提交都被拦。
步骤 6:跑之前先排除干扰项
度量时务必:关闭对象缓存的首次冷启动影响(先访问一次预热);选择内容量中等的文章页而非首页(首页往往有缓存和特殊优化);确认没有插件正在跑后台任务(wp cron event list 看一下);本机不要同时跑编译或备份任务。
常见踩坑
- 用桌面端结果汇报移动端优化:两者分数差距可达 20 分以上,必须声明策略(strategy=mobile)。
- 在装了广告/统计插件的页面上测:第三方脚本的加载时间波动大,会淹没主题本身的改动。度量期间用
--blocked-url-patterns屏蔽。 - 只跑首页:首页常有整页缓存,测出来的是缓存性能不是主题性能。至少覆盖首页、文章页、归档页三类模板。
- 把 Lab 分数当 KPI:分数是加权合成值,改一个不影响体验的小项也能涨分。应盯 LCP / CLS / TBT 三个原始指标。
- 案例: 某站点优化后 Lab 分数从 58 涨到 88,团队认为大功告成。一个月后 CrUX 数据显示 LCP 仍在 2.9s(「需要改进」区间),原因是真实访客里 70% 是低端安卓机,而优化只做了图片格式转换,没处理首屏渲染阻塞的 JS。教训:Lab 提升但 Field 不动时,说明优化方向偏离了真实瓶颈,应回到 Field 数据的分布上看设备构成。
- 没预留预热请求:第一次跑必然包含 DNS、TLS、对象缓存冷启动,分数偏低 10 分左右,被误当成优化效果。
- 改了多个东西后一次性测量:无法归因。每次只改一项、测一次,哪怕多花时间。
总结
可交付的性能度量由四部分组成:固定模拟参数(移动端预设 + throttlingMethod: simulate)、五轮取中位数过滤噪声、把每次结果按版本号存成基线、以及接入 PageSpeed Insights API 跟踪 CrUX 的 28 天真实数据。
判断改动是否有效的硬标准是:LCP 改善 ≥200ms 或 CLS 改善 ≥0.05,且 CrUX 在一个完整 28 天窗口后评级上升;只涨分数但原始指标不动的改动不算优化。度量确定瓶颈后,具体的减法动作参考 主题加载的资源请求数精简清单,改动过程按 自己维护主题的版本管理与变更记录 记录版本号,保证每次性能变化都能追溯到具体提交。
