起因
新 landing page 在 Lighthouse Mobile 上跑只有 45 分(红色)。
boss 看到 PageSpeed Insights 截图骂街。
"哪些指标拉低分数?怎么改?" 系统化地走一遍。
6 个关键指标
Lighthouse 综合分基于:
| 指标 | 解释 | 目标 |
|---|---|---|
| LCP (Largest Contentful Paint) | 首屏最大元素显示时间 | < 2.5s |
| INP (Interaction to Next Paint) | 用户交互响应延迟 | < 200ms |
| CLS (Cumulative Layout Shift) | 累计布局偏移 | < 0.1 |
| FCP (First Contentful Paint) | 任何内容首次显示 | < 1.8s |
| Speed Index | 视觉填充速度 | < 3.4s |
| TBT (Total Blocking Time) | 主线程被阻塞总时长 | < 200ms |
LCP / INP / CLS 是 "Core Web Vitals",Google 排名因子。
我做的 7 个改动
1. 给关键图片加 fetchpriority="high" 和 preload
<head>
<link rel="preload" as="image" href="/hero.jpg" fetchpriority="high">
</head>
<body>
<img src="/hero.jpg" fetchpriority="high" loading="eager"
width="1200" height="600" alt="...">
</body>
之前 hero 图被各种 CSS / JS 排到后面,LCP 4s。
preload + high priority 后 1.2s。
2. 字体 preload + font-display: swap
<link rel="preload" as="font" type="font/woff2"
href="/fonts/inter.woff2" crossorigin>
<style>
@font-face {
font-family: 'Inter';
src: url('/fonts/inter.woff2') format('woff2');
font-display: swap; /* 字体没下来用 fallback,不阻塞渲染 */
}
</style>
之前自定义字体阻塞文字渲染 800ms。改后字体没到先用 system fallback。
3. 关闭未使用的第三方脚本
PageSpeed 的 "Reduce unused JavaScript" 报告告诉我 Google Analytics +
Hotjar + Intercom 注入了 280KB JS。
- Hotjar 临时关(运营暂时不需要)
- Intercom 改 "页面停留 > 10s 才加载"
- GA 切到
gtag.js(最小版)
JS bundle 从 320 KB → 80 KB。
4. CSS critical path:inline above-the-fold + defer 其余
<head>
<style>
/* 首屏关键 CSS inline 进 HTML */
body{font-family:system-ui;...} .hero{...} .nav{...}
</style>
<link rel="preload" href="/main.css" as="style"
onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/main.css"></noscript>
</head>
之前 main.css 阻塞 render 600ms。inline critical CSS 后渲染立刻开始。
工具:critters / critical npm 包能自动提取 critical CSS。
5. 图片用 AVIF / WebP + responsive srcset
<picture>
<source srcset="/hero-800.avif 800w, /hero-1600.avif 1600w"
sizes="100vw" type="image/avif">
<source srcset="/hero-800.webp 800w, /hero-1600.webp 1600w"
sizes="100vw" type="image/webp">
<img src="/hero-800.jpg" alt="" width="1600" height="800"
loading="eager" fetchpriority="high">
</picture>
AVIF 比 JPEG 小 50%+。手机用 800w 版本,平板用 1600w。
图片总下载量从 1.2 MB → 300 KB。
6. 移除阻塞渲染的 JS(defer / async)
<!-- 之前 -->
<script src="/analytics.js"></script> <!-- 阻塞 -->
<script src="/vendor.js"></script> <!-- 阻塞 -->
<!-- 现在 -->
<script src="/main.js" defer></script> <!-- 等 HTML parse 完才执行 -->
<script src="/analytics.js" async></script><!-- 不阻塞,下载完就跑 -->
defer:HTML parse 完 + DOMContentLoaded 前按顺序执行async:下载完立刻执行,不保顺序- 不带:阻塞 parser(最差)
7. CLS:所有 image / iframe 写 width/height
<!-- 错 -->
<img src="/photo.jpg" alt=""> <!-- 加载完撑高页面 → 跳动 -->
<!-- 对 -->
<img src="/photo.jpg" width="800" height="600" alt="">
让浏览器在图片下载完前知道占位空间。CSS 用 max-width:100%; height:auto
保持响应式。
CLS 从 0.34 → 0.02。
跑 Lighthouse
Chrome DevTools → Lighthouse → 选 Mobile + Performance → Generate report.
或者命令行 CI 跑:
npm i -D lighthouse @lhci/cli
lhci autorun --upload.target=temporary-public-storage
@lhci/cli 能跑多次取中位数 + 上传报告 + PR 评论分数变化。
结果
| 改前 | 改后 | |
|---|---|---|
| Performance | 45 | 95 |
| LCP | 4.2s | 1.4s |
| INP | 380ms | 120ms |
| CLS | 0.34 | 0.02 |
| TBT | 850ms | 110ms |
| JS bundle | 320 KB | 80 KB |
| 首屏图片 | 1.2 MB | 300 KB |
持续优化
CI 限红线
# lighthouse.config.js
module.exports = {
ci: {
assert: {
assertions: {
'categories:performance': ['error', { minScore: 0.9 }],
'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
},
},
},
}
LCP > 2.5s 的 PR 直接 fail。防止性能慢慢退化。
真实用户数据:web-vitals + analytics
import { onCLS, onINP, onLCP, onFCP, onTTFB } from 'web-vitals'
const send = (metric) => {
navigator.sendBeacon('/analytics/vitals', JSON.stringify({
name: metric.name,
value: metric.value,
id: metric.id,
}))
}
onCLS(send)
onINP(send)
onLCP(send)
onFCP(send)
onTTFB(send)
收集真实用户的 web vitals 上报到自己的分析后端。Lighthouse 是合成测试,
真实用户在各种设备 / 网络下的体验才是终极目标。
踩过的坑
-
测试环境 vs 生产:dev server 没 gzip / 没缓存 → 跑 Lighthouse
分数极低。永远在 staging / prod-like build 上跑。 -
fetchpriority="high"滥用:所有 img 都设 high → 优先级失效。
只首屏关键 1-2 个图设。 -
inline critical CSS 太多:> 14 KB(一个 TCP 窗口)反而慢,因为
阻塞了 HTML 后续 stream。生产建议 < 10 KB。 -
Hydration 慢拉低 INP:React app 大 bundle hydrate 时主线程被
占。RSC + 把交互组件拆细减少 hydration scope。 -
PSI mobile 分数比 Desktop 低很多:移动用 throttled CPU + 慢 4G。
永远以 mobile 为准(用户主要在手机)。
登录后参与评论。