学完这篇,你能看懂 LCP、FID、CLS 这三个指标到底在量什么,并知道从哪下手改代码,让页面打开更快、内容不跳动、点击不卡顿。
第一步:先搞清楚三个指标各自量的是什么
这三个指标是 Google 的 Core Web Vitals(核心网页指标),作用是量化用户真实感受到的加载体验:
| 指标 | 全称 | 衡量什么 | 良好 | 需要改进 | 差 |
|---|
| LCP | Largest Contentful Paint | 最大内容元素渲染完成的时间 | ≤2.5s | 2.5–4s | >4s |
| FID | First Input Delay | 首次交互到浏览器响应的延迟 | ≤100ms | 100–300ms | >300ms |
| CLS | Cumulative Layout Shift | 页面元素意外位移的累积分数 | ≤0.1 | 0.1–0.25 | >0.25 |
LCP 管“快不快”,FID 管“灵不灵”,CLS 管“稳不稳”。三者互不替代,任何一个差都会拉低整体体验。
注意:FID 从 2024 年 3 月起已被 INP(Interaction to Next Paint)取代,不再是官方核心指标。但两者的优化手段高度重合,本文按 FID 讲思路,同时给出 INP 的补充做法。新项目请直接盯着 INP(良好阈值 ≤200ms)。
第二步:先测,别凭感觉改
优化前必须有基线数据,否则你不知道改完是变好还是变坏。
实验室测量(本地开发用):
- 打开 Chrome 无痕窗口,按
F12 打开 DevTools
- 切到
Lighthouse 面板,勾选 Performance,Device 选 Mobile,点 Analyze page load
- 报告里直接能看到 LCP、CLS、TBT(TBT 可近似代替 FID)的分项和具体拖后腿的资源
真实用户测量(线上必须有):
<script type="module">
import {onLCP, onCLS, onINP} from 'https://unpkg.com/web-vitals@4?module';
onLCP(console.log);
onCLS(console.log);
onINP(console.log);
</script>
实测数据要发回自己的统计接口,按天聚合看 75 分位值——实验室测的是“理想网络下的单次结果”,真实用户的 4G 手机才是真相。
注意:本地开发机性能强、网络快,Lighthouse 分数常年偏乐观。一定要用 DevTools 的 Network 面板把网络限速调到 Fast 3G、CPU 调到 4x slowdown 再测。
第三步:优化 LCP——让主内容早点出现
在 DevTools Performance 面板录制一次加载,找 LCP 标记对应的元素(通常是首屏大图、大标题或视频封面)。然后按顺序排查:
1)服务端响应时间(TTFB)
LCP 不可能早于 TTFB。开缓存、上 CDN、数据库加索引,先把首字节时间压到 800ms 以内。
2)图片是 LCP 元素时
<!-- 首屏主图:提前发现 + 高优先级 + 明确尺寸 -->
<link rel="preload" as="image" href="/hero.webp" fetchpriority="high">
<img src="/hero.webp" width="1200" height="600" fetchpriority="high" alt="首图">
同时把图压到 WebP/AVIF、按显示尺寸输出、首屏图不要用 loading="lazy"(懒加载会让它推迟到很晚才加载)。
3)CSS/JS 阻塞渲染
把首屏必需的 CSS 内联进 <head>,其余用 <link rel="stylesheet" media="print" onload="this.media='all'"> 异步加载;<script> 一律加 defer 或 async,别放渲染路径上。
4)字体拖慢文本 LCP
@font-face {
font-family: 'MyFont';
src: url('/font.woff2') format('woff2');
font-display: swap; /* 先用系统字体渲染,字体到了再换 */
}
第四步:优化 CLS——别让内容乱跳
CLS 的成因几乎全是“元素先占位、后变尺寸”或“内容被插到已有内容上方”。改法很固定:
1)图片、视频、iframe 必须写死宽高
<img src="a.jpg" width="800" height="450" alt="">
<iframe src="..." width="560" height="315"></iframe>
或者用 CSS 占位:
.thumb { aspect-ratio: 16 / 9; width: 100%; }
2)广告位、推荐位、公告条预留固定高度
不要等接口返回再撑开容器,先给一个 min-height。
3)动态插入的内容不要放在可视区上方
比如“登录成功”提示条从顶部推下来,会把整页内容顶动。改成 position: fixed 浮层或绝对定位。
4)动画只用 transform 和 opacity
改 width、height、top、margin 会触发重排,容易连带产生位移。
注意:CLS 是累积值,页面加载后 5 秒内的位移都算。用户滚动触发的位移不算在内——判断标准是“非用户操作引起”。
第五步:优化 FID / INP——别让主线程堵住
FID 的本质是:用户点击时,主线程正在跑长任务(超过 50ms 的 JS),事件只能排队。所以思路就是把长任务切碎、把不必要的 JS 拿掉。
1)先减量
用 DevTools Coverage 面板看 JS 和 CSS 的未使用率,通常能砍掉 40% 以上的代码。第三方脚本(统计、客服、广告)一律 async 或延后到 load 之后再注入。
2)拆分长任务
async function chunkedWork(items) {
for (const item of items) {
process(item);
// 让出主线程,给用户交互一个响应窗口
await new Promise(r => setTimeout(r, 0));
}
}
优先用 scheduler.yield()(新浏览器已支持),降级用 setTimeout。
3)事件处理做防抖/节流
滚动、输入、resize 里的重活,用 requestAnimationFrame 或 100ms 防抖包一层。
4)大列表用虚拟滚动或分页
一次渲染上千个 DOM 节点,主线程直接就卡住了。
注意:INP 比 FID 更严格——它不只算“首次”交互,还统计整个页面生命周期内所有交互的响应时间(取高值)。如果页面用久了越来越卡,先查内存泄漏和不断累积的事件监听器。
第六步:改完要回归验证
每改一项,重新跑一次 Lighthouse,并对比线上真实用户数据的 75 分位。建议把三个指标做成监控看板,按周看趋势——性能优化是长期保持的工作,不是一次性任务。
小结
- LCP 抓首屏最大元素:压 TTFB、预加载主图、
fetchpriority="high"、CSS/JS 不阻塞渲染、font-display: swap
- CLS 抓意外位移:所有图片/视频/iframe 写死宽高或用
aspect-ratio 占位,动态内容别插到上方,动画只用 transform/opacity
- FID/INP 抓长任务:砍第三方 JS、拆分超 50ms 的任务、大列表虚拟滚动
- 先测后改:Lighthouse 看实验室数据,
web-vitals 看真实用户 75 分位,两者结合才准
- FID 已退役,新项目直接盯 INP(≤200ms)