渲染与性能
Core Web Vitals:LCP、INP、CLS 怎么查、怎么改
三个指标分别回答“内容多久出来”“点了有没有反应”“页面会不会乱跳”。先学会看真实用户的数据,再逐项优化。
三个指标
| 指标 | 回答的问题 | 良好 | 较差 |
|---|---|---|---|
| LCP 最大内容绘制 | 主要内容多久显示出来? | ≤ 2.5 秒 | > 4 秒 |
| INP 交互到下一次绘制 | 点击后多久有反馈? | ≤ 200 毫秒 | > 500 毫秒 |
| CLS 累积布局偏移 | 页面元素会不会突然移位? | ≤ 0.1 | > 0.25 |
评估时看的是第 75 百分位:75% 的访问都达标,才算这个页面达标。INP 在 2024 年 3 月取代了之前的 FID,因为 FID 只衡量首次交互的输入延迟,而 INP 观察整个访问过程中的交互。
先查数据
指标分两类:
- 实验室数据(Lighthouse、DevTools):在固定条件下模拟,适合调试。
- 真实用户数据(Chrome 用户体验报告、自己采集):反映真实的设备和网络,是最终标准。
PageSpeed Insights 可以同时看到这两类数据。想自己采集,用 Google 的 web-vitals 库最省事:
import { onLCP, onINP, onCLS } from 'web-vitals';
function report(metric) {
navigator.sendBeacon('/analytics', JSON.stringify(metric));
}
onLCP(report);
onINP(report);
onCLS(report);优化 LCP
LCP 的时间可以拆成四段:服务器响应(TTFB)、资源开始加载前的等待、资源下载、渲染。先看哪一段最长,再对症下药。
最常见的情况是 LCP 元素是一张首屏大图,这时:
<!-- 首屏大图:提高优先级,不要懒加载 -->
<img src="hero.avif" width="1200" height="630" fetchpriority="high" alt="产品界面截图">
<!-- 首屏以下的图片才用懒加载 -->
<img src="detail.avif" width="600" height="400" loading="lazy" alt="细节图">- **不要给 LCP 图片加
loading="lazy"**,这是很常见的错误。 - CSS 背景图要等样式解析后才被发现,首屏大图最好用
<img>,或者用<link rel="preload" as="image">提前声明。 - 使用 AVIF、WebP 等现代格式,并用
srcset提供合适的尺寸。 - 减少阻塞渲染的 CSS 和脚本。
- 用 CDN 和缓存降低 TTFB。
优化 INP
INP 由三部分组成:输入延迟(主线程在忙,事件要排队)、事件处理、绘制下一帧。优化的核心只有一条:不要长时间占住主线程。
- 拆分长任务,定期让出主线程。详见事件循环一文。
- 先给反馈,再做计算。比如先把按钮状态改掉,让浏览器绘制一帧之后再执行耗时逻辑:
button.addEventListener('click', () => {
button.textContent = '处理中…';
requestAnimationFrame(() => {
setTimeout(doHeavyWork); // 下一帧绘制之后再开始
});
});- 减少第三方脚本,移除没用到的代码。
- 大列表使用虚拟滚动,或者用
content-visibility降低渲染成本。
优化 CLS
布局偏移几乎都来自“内容出现时才占位”。
- **图片和视频写上
width和height**,或者设置aspect-ratio,浏览器就能提前留好位置。 - 广告、嵌入内容和动态内容预留空间,不要在现有内容上方插入新内容。
- 控制字体切换:Web 字体加载后可能改变文字尺寸。可以预加载关键字体,并用
size-adjust让备用字体尺寸接近。 - **动画用
transform**:它不会引起布局偏移。
用户交互后 500 毫秒内发生的偏移不计入 CLS,比如点击展开面板是没有问题的。
怎样定位具体元素
想知道页面的 LCP 元素是谁,可以用 PerformanceObserver 直接观察:
new PerformanceObserver((list) => {
const entries = list.getEntries();
const last = entries[entries.length - 1];
console.log('LCP', last.startTime, last.element);
}).observe({ type: 'largest-contentful-paint', buffered: true });把 type 换成 layout-shift,就能看到每次布局偏移及其来源。