“这个页面有点慢”是一句没法行动的话。慢在哪?是白屏太久,还是点了没反应,还是滚动时画面乱跳?性能优化要落地,第一步是把模糊的”慢”拆成可测量的指标。这篇就从关键渲染路径讲到 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。它们分别对应三个朴素的问题:
| 指标 | 全称 | 回答的问题 | 良好阈值 |
|---|---|---|---|
| LCP | Largest Contentful Paint | 主要内容多久能看到? | ≤ 2.5s |
| INP | Interaction to Next Paint | 交互后多久有反馈? | ≤ 200ms |
| CLS | Cumulative Layout Shift | 画面跳不跳? | ≤ 0.1 |
逐个来看它们衡量什么、以及怎么优化。
LCP:最大内容绘制——“我看到了吗”↗
LCP 记录的是视口内最大的那个元素(通常是主图或大标题)完成渲染的时刻。它约等于用户主观上”页面加载好了”的时间点。
LCP 慢,通常慢在这几处:
- 服务器响应慢(TTFB 高)→ 用 CDN、加缓存、上服务端渲染或静态化。
- 关键资源加载慢 → 压缩图片、用现代格式(WebP/AVIF)、
preload首屏大图。 - 渲染被阻塞 → 精简阻塞的 CSS 和 JS(回到关键渲染路径)。
一个立竿见影的动作:给首屏主图去掉懒加载。懒加载对首屏图反而有害,因为它推迟了 LCP 元素的加载:
<!-- 首屏主图:不要懒加载,反而要提示浏览器高优先级 -->
<img src="/hero.avif" fetchpriority="high" alt="..." />
<!-- 屏幕外的图:才用懒加载 -->
<img src="/below-fold.avif" loading="lazy" alt="..." />
INP:下一次绘制的交互延迟——“点了有反应吗”↗
INP 衡量的是:用户交互(点击、输入、按键)之后,页面多久才更新出下一帧。它取代了旧的 FID,更全面地反映了整个会话的响应能力。
INP 差,几乎总是因为主线程被长任务堵住了——用户点了按钮,但主线程正忙着执行一段几百毫秒的 JS,没空处理点击。优化方向:
- 拆分长任务,主动让出主线程(
setTimeout、scheduler.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,卡在首屏大图上”——一句可以立刻行动的话。这才是性能优化该有的样子:不玄学,有靶子,可验证。