Rucky 前端笔记

渲染与性能

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 库最省事:

JavaScript
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 元素是一张首屏大图,这时:

HTML
<!-- 首屏大图:提高优先级,不要懒加载 -->
<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 由三部分组成:输入延迟(主线程在忙,事件要排队)、事件处理、绘制下一帧。优化的核心只有一条:不要长时间占住主线程。

  • 拆分长任务,定期让出主线程。详见事件循环一文。
  • 先给反馈,再做计算。比如先把按钮状态改掉,让浏览器绘制一帧之后再执行耗时逻辑:
JavaScript
button.addEventListener('click', () => {
  button.textContent = '处理中…';
  requestAnimationFrame(() => {
    setTimeout(doHeavyWork); // 下一帧绘制之后再开始
  });
});
  • 减少第三方脚本,移除没用到的代码。
  • 大列表使用虚拟滚动,或者用 content-visibility 降低渲染成本。

优化 CLS

布局偏移几乎都来自“内容出现时才占位”。

  • **图片和视频写上 width 和 height**,或者设置 aspect-ratio,浏览器就能提前留好位置。
  • 广告、嵌入内容和动态内容预留空间,不要在现有内容上方插入新内容。
  • 控制字体切换:Web 字体加载后可能改变文字尺寸。可以预加载关键字体,并用 size-adjust 让备用字体尺寸接近。
  • **动画用 transform**:它不会引起布局偏移。

用户交互后 500 毫秒内发生的偏移不计入 CLS,比如点击展开面板是没有问题的。

怎样定位具体元素

想知道页面的 LCP 元素是谁,可以用 PerformanceObserver 直接观察:

JavaScript
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,就能看到每次布局偏移及其来源。

延伸阅读