7AA7 博客

Core Web Vitals 优化:LCP、INP、CLS 的成因与手段

温言 UID:115
2026-09-05 08:29
30 2 0

三个指标其实在回答三个不同问题:内容多久能看到(LCP)、操作多久有响应(INP)、页面视觉稳不稳(CLS)。它们各自的成因几乎不重叠,优化手段也必须分开谈。

三个指标各自衡量什么

  • LCP(Largest Contentful Paint):视口内最大内容元素(图片、视频封面、块级文本)的渲染时间。衡量”主要内容加载完成”的感知速度。
  • INP(Interaction to Next Paint):从用户交互到下一帧绘制出来的延迟,取页面生命周期内所有交互的近似最大值。衡量交互响应能力。
  • CLS(Cumulative Layout Shift):所有意外布局偏移的累计分数。衡量视觉稳定性。

INP 与前身 FID 的本质区别:FID 只测首次输入的等待时间,INP 测的是完整往返——包括事件处理本身和之后的渲染。一个处理了 300ms 的点击在 FID 下很健康,在 INP 下不合格。

LCP 的四个子阶段

LCP 时间是四段相加的结果,拆开才定位得准:

子阶段 含义 典型问题 手段
TTFB 首字节到达 服务端慢、重定向链、无 CDN 边缘缓存、SSR/流式响应、减少重定向
资源加载延迟 发现 LCP 资源到开始下载 资源被 JS 动态插入、懒加载误用 提前声明、<link rel="preload">、别懒加载首屏图
资源加载时长 下载耗时 体积大、未压缩、图太重 AVIF/WebP、响应式 srcset、合适的压缩率
渲染延迟 资源就绪到绘制 渲染被阻塞、主线程忙 内联关键 CSS、推迟非关键 JS、拆长任务

最常见的坑是第二项:<img> 被一个 React 组件在 useEffect 里塞进 DOM,浏览器要等 JS 下载、解析、执行完才知道要下载这张图,白白浪费一个 RTT 以上。

<!-- 首屏主图:不要 lazy,要主动提高优先级 -->
<link rel="preload" as="image" href="hero.webp" fetchpriority="high">
<img src="hero.webp" fetchpriority="high" width="1200" height="600" alt="">

另一个高频问题:fetchpriority="high" 之外,还要注意 <img> 默认优先级其实不高,浏览器会先抓 CSS 和 JS。手动提权往往立竿见影。

LCP 元素在后续也可能变化——比如一张更大的图稍后加载完,LCP 会被更新。所以 PerformanceObserver 要持续监听直到页面隐藏。

INP 与长任务:主线程是唯一瓶颈

INP 的三段构成:输入延迟(input delay)+ 处理时间(processing)+ 呈现延迟(presentation)。

绝大多数 INP 问题出在后两段。根本原因一致:主线程一次只能干一件事。一个 500ms 的长任务期间,用户的点击只能排队等着。

// 把长循环切片,让出主线程
async function processAll(list) {
  for (let i = 0; i < list.length; i++) {
    if (i % 100 === 0) {
      await new Promise(r => setTimeout(r, 0)); // 一种让出方式
    }
    heavyStep(list[i]);
  }
}

更现代的做法是用 scheduler.yield()(可用时用,不可用时退化为 setTimeout)。它的优势是让出后回调排在队列靠前位置,比 setTimeout(0) 更快被重新调度。

const yieldToMain = () =>
  'scheduler' in globalThis && 'yield' in scheduler
    ? scheduler.yield()
    : new Promise(r => setTimeout(r, 0));

诊断手段是 Long Tasks API,把超过 50ms 的任务记下来:

new PerformanceObserver(list => {
  for (const e of list.getEntries()) {
    report({ type: 'longtask', duration: e.duration, name: e.name });
  }
}).observe({ type: 'longtask', buffered: true });

还要留意事件回调里的同步布局读取——它把”处理时间”和”呈现延迟”一起放大。以及第三方脚本:聊天插件、埋点 SDK、A/B 测试框架是长任务的重灾区,值得逐个测量而不是全盘接受。

CLS 的常见元凶

布局偏移分数的计算方式是 impact fraction × distance fraction:受影响视口比例乘以位移距离比例。哪怕是巨大区域移动 1px,分数也可能不低。

四类经典成因:

  1. 无尺寸的图片/视频/广告位:内容到达时把下方推开。解法是写死 width/height 或用 aspect-ratio:
.thumb { aspect-ratio: 16 / 9; width: 100%; }
  1. 网页字体切换:Fallback 字体与自定义字体的度量不同,文本换行位置变化导致高度跳变。解法是 font-display: optional(不产生切换期)或用 size-adjust、ascent-override 让 fallback 的度量对齐目标字体。

  2. 动态插入内容:Cookie 提示条、顶部通知插入文档流上方,把整页往下顶。解法是预留空间,或用 position: fixed 浮层——注意浮层本身不计入 CLS,但它遮挡的交互会影响体验。

  3. 动画走布局属性:用 top/height 做动画。改成 transform 就不产生布局偏移。

/* 会产生 CLS */
.panel { transition: top 0.3s; }
/* 不会 */
.panel { transition: transform 0.3s; }

还有一个容易被忽略的来源:SPA 路由切换时新页面内容高度未知,滚动位置与内容错位。这需要用真实的路由级指标去观测,而不是只看首次加载。

回访用户是另一个战场

第一次访问受限于网络与冷缓存,回访用户受限于缓存策略和启动逻辑。只优化冷启动的结果是回访体验并没有跟着改善。

要点是:带内容哈希的静态资源可以设较长的 max-age,HTML 本身要 no-cache 或短缓存,Service Worker 的更新策略要避免”永远命中旧版本”。用 PerformanceNavigationTiming 的 type 字段把 navigate、reload、back_forward 分开统计,才能看清三类访问各自的指标。

浏览器前进后退缓存(bfcache)是最容易被浪费的性能红利:命中时页面从内存恢复,几乎零成本。常见的破坏因素是 unload 监听、未关闭的 WebSocket 或 IndexedDB 连接、以及某些 Cache-Control: no-store 设置。命中率值得单独作为一个监控项。

实验室数据与真实数据的分歧

Lighthouse 跑在特定的网络节流与 CPU 降速设定下,是一种受控复现。它适合做 A/B 对比和回归检测,不适合直接当作用户体感。分歧来源有几种。

一是设备分布。实验室通常用中端配置的模拟,真实用户里有大量低端安卓机,JS 执行时间可能是实验室的数倍,INP 的差距往往远大于 LCP 的差距。

二是网络形态。实验室假设稳定带宽加固定延迟,真实用户处在弱信号、频繁切换 WiFi 与蜂窝、丢包重传的环境里,TTFB 的方差极大。

三是用户行为。实验室只测首次加载的冷启动路径,真实用户会带着缓存回来、会在页面还没加载完就开始点击、会同时开着多个标签页争抢主线程。”加载中途就交互”这一类实验室几乎测不到,而这恰恰是 INP 最差的场景。

四是第三方脚本的差异。实验室环境里广告、埋点、客服插件可能因为被拦截而完全没跑,真实用户那边却是完整的。

用真实用户数据决定优先级

实验室数据(Lighthouse)只有一个网络与设备组合,它的价值在于复现与对比;真实用户监控(RUM)才告诉你用户实际处在什么状况。

import { onLCP, onINP, onCLS } from 'web-vitals';
onLCP(m => send({ name: 'LCP', value: m.value, rating: m.rating, target: m.entries.at(-1)?.element?.tagName }));

上报时带上归因维度(LCP 元素类型、INP 的目标选择器、CLS 的偏移源),比只报一个数字有用得多。

判断优先级的思路:

  • 看分布而非均值。P75 达标而 P95 崩掉,说明特定人群或特定页面有问题;
  • 按页面分组。全站均值常被流量最大的首页掩盖,真正糟糕的可能是下单页;
  • 按设备分档。低端安卓机上 INP 恶化往往源自 JS 执行时间,与中高端机不是同一套解法;
  • 指标与业务指标做交叉。LCP 改善但转化没动,说明瓶颈在别处。

一个务实的顺序:先修那些”改法明确、收益确定”的项(图片尺寸、首屏图优先级、字体度量),再去攻坚长任务拆分这类需要重构的工作。前者通常能让 P75 明显下移,后者决定长尾。

1.2k浏览量
30赞
2评论
2参与用户
11 分钟阅读时间

评论(2)

  • Bit 序章 · 2026-09-05 10:32

    感觉作者是被生产事故教育过的。

    回复
  • 温言 · 2026-09-05 19:36

    这个坑我踩过两次,第一次怪框架,第二次怪自己。

    回复