前端性能:关键渲染路径与 Core Web Vitals

性能优化不该是玄学。把'用户什么时候看到内容、什么时候能交互'量化成几个指标,优化就有了明确的靶子。

“这个页面有点慢”是一句没法行动的话。慢在哪?是白屏太久,还是点了没反应,还是滚动时画面乱跳?性能优化要落地,第一步是把模糊的”慢”拆成可测量的指标。这篇就从关键渲染路径讲到 Google 的 Core Web Vitals,把优化变成有靶子的事。

关键渲染路径:首屏的最短链路

用户从输入网址到看见内容,中间有一条关键渲染路径(Critical Rendering Path, CRP)。它是首屏能多快出现的决定性因素,环节大致是:

请求 HTML
  → 解析 HTML,遇到 CSS/JS 等资源
  → 下载并处理 CSS(构建 CSSOM,阻塞渲染)
  → 下载并执行阻塞的 JS
  → 构建渲染树 → 布局 → 绘制 → 首屏出现

优化关键渲染路径,本质就是缩短这条链路上的三件事:请求数、字节数、和关键资源的处理时间。 具体手段包括:

  • 内联首屏关键 CSS,其余 CSS 异步加载,避免整包 CSS 阻塞渲染。
  • 给脚本加 defer,别让 JS 阻塞 HTML 解析(原理见另一篇《浏览器渲染流水线》)。
  • 预连接与预加载关键资源:<link rel="preconnect"><link rel="preload">
<head>
  <!-- 提前和字体/图片 CDN 建立连接,省去后续握手时间 -->
  <link rel="preconnect" href="https://cdn.example.com" />
  <!-- 首屏必需的字体,提高优先级 -->
  <link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin />
</head>

Core Web Vitals:用户视角的三个指标

Google 把”用户实际感受到的性能”提炼成了三个核心指标,合称 Core Web Vitals。它们分别对应三个朴素的问题:

指标全称回答的问题良好阈值
LCPLargest Contentful Paint主要内容多久能看到?≤ 2.5s
INPInteraction to Next Paint交互后多久有反馈?≤ 200ms
CLSCumulative Layout Shift画面跳不跳?≤ 0.1

逐个来看它们衡量什么、以及怎么优化。

LCP:最大内容绘制——“我看到了吗”

LCP 记录的是视口内最大的那个元素(通常是主图或大标题)完成渲染的时刻。它约等于用户主观上”页面加载好了”的时间点。

LCP 慢,通常慢在这几处:

  1. 服务器响应慢(TTFB 高)→ 用 CDN、加缓存、上服务端渲染或静态化。
  2. 关键资源加载慢 → 压缩图片、用现代格式(WebP/AVIF)、preload 首屏大图。
  3. 渲染被阻塞 → 精简阻塞的 CSS 和 JS(回到关键渲染路径)。

一个立竿见影的动作:给首屏主图去掉懒加载。懒加载对首屏图反而有害,因为它推迟了 LCP 元素的加载:

<!-- 首屏主图:不要懒加载,反而要提示浏览器高优先级 -->
<img src="/hero.avif" fetchpriority="high" alt="..." />

<!-- 屏幕外的图:才用懒加载 -->
<img src="/below-fold.avif" loading="lazy" alt="..." />

INP:下一次绘制的交互延迟——“点了有反应吗”

INP 衡量的是:用户交互(点击、输入、按键)之后,页面多久才更新出下一帧。它取代了旧的 FID,更全面地反映了整个会话的响应能力。

INP 差,几乎总是因为主线程被长任务堵住了——用户点了按钮,但主线程正忙着执行一段几百毫秒的 JS,没空处理点击。优化方向:

  • 拆分长任务,主动让出主线程(setTimeoutscheduler.yield(),见《事件循环》一篇)。
  • 减少不必要的渲染:避免大范围重排、收敛无谓的状态更新。
  • 把重计算移出主线程:Web Worker 是处理数据密集型任务的正解。
// 把耗时计算丢给 Worker,主线程保持对交互的响应
const worker = new Worker('/heavy.js');
worker.postMessage(bigData);
worker.onmessage = (e) => render(e.data);

CLS:累积布局偏移——“画面会乱跳吗”

CLS 衡量页面在加载过程中,元素意外位移的累积程度。典型的糟糕体验是:你正要点一个按钮,上方突然插进来一张加载完的图片,把按钮挤下去,你点错了。

CLS 的成因和对策非常明确:

成因对策
图片/视频没写尺寸显式写 width/height 或用 aspect-ratio 占位
广告、嵌入内容动态插入提前预留固定高度的容器
网页字体加载导致文字重排font-display: optional/swap 并预加载字体
/* 用宽高比给图片预留空间,加载完也不会挤动布局 */
img {
  aspect-ratio: 16 / 9;
  width: 100%;
  height: auto;
}

度量优先于优化

最后强调一个原则,也是整篇的落点:先测量,再优化。 凭感觉优化很容易把精力花在不痛的地方。可用的工具:

  • Lighthouse / PageSpeed Insights:实验室数据,快速定位问题。
  • Chrome DevTools 的 Performance 面板:录制一段交互,看主线程上到底是谁在占用时间。
  • web-vitals 库:在真实用户环境里采集 LCP / INP / CLS,因为实验室数据和真实网络、真实设备往往差很远。
import { onLCP, onINP, onCLS } from 'web-vitals';

onLCP(console.log);
onINP(console.log);
onCLS(console.log);

把这套流程走通,“页面有点慢”就会变成”LCP 是 4.2s,卡在首屏大图上”——一句可以立刻行动的话。这才是性能优化该有的样子:不玄学,有靶子,可验证。


More like this