← 返回博客首页

Web 性能优化完全指南:从 Core Web Vitals 到落地清单

性能是业务指标,不是技术指标

先建立一个共识:性能优化不是工程师的自我修养,它直接对应营收。业界反复验证过的相关性包括:

  • 加载时间每增加 1 秒,移动端转化率显著下降
  • 沃尔玛实测:加载时间每改善 1 秒,转化率提升约 2%
  • Pinterest 把感知等待时间降低 40% 后,搜索引擎流量与注册量提升约 15%

(具体数字因行业和基线而异,别照搬;但方向高度一致:更快的页面 = 更高的转化。)

除了转化,性能还直接影响 SEO——Google 明确把 Core Web Vitals 作为排名信号之一。这使得性能从"锦上添花"变成"必须达标"。

缓存与压缩相关的响应头,可对照本站的 HTTP 头速查表 逐项核对。

Core Web Vitals:先搞清楚在测什么

指标 衡量什么 良好 需改进
LCP Largest Contentful Paint 加载速度:最大内容元素渲染出来的时间 ≤ 2.5s 2.5–4s > 4s
INP Interaction to Next Paint 交互响应:用户操作到画面更新的延迟 ≤ 200ms 200–500ms > 500ms
CLS Cumulative Layout Shift 视觉稳定:内容意外位移的累积量 ≤ 0.1 0.1–0.25 > 0.25

三个关键细节:

  1. INP 已于 2024 年 3 月正式取代 FID。FID 只衡量首次交互的输入延迟(不含处理与渲染),严重低估真实卡顿;INP 衡量几乎所有交互的完整延迟。如果你的资料还在讲 FID,已经过时了。
  2. 取第 75 百分位。不是平均值——四分之三的真实访问都必须达标。这意味着平均值好看没用,长尾用户体验决定成败。
  3. 三个指标要同时满足,缺一个就是"未通过"。

实验室数据 vs 真实用户数据

数据来源 特点 用途
CrUX / GSC 核心网页指标报告 真实用户,28 天滚动,按设备分组 判达标(唯一依据)
Lighthouse / PageSpeed Insights 实验室,固定条件,单次运行 定位问题、看优化建议
WebPageTest 实验室,可调网络/设备/地理位置 深入分析、看瀑布图

两者不一致是常态,原因见文末 FAQ。

关键渲染路径:首屏到底被谁拖住

浏览器的首屏渲染大致要经过:解析 HTML → 构建 DOM → 加载并解析 CSS → 构建 CSSOM → 执行 JS → 构建渲染树 → 布局 → 绘制。

其中会阻塞渲染的只有两类资源:CSS 和同步 JS。

CSS 必须阻塞,但要把范围压到最小

CSS 会阻塞渲染(否则会出现无样式内容闪烁 FOUC)。正确做法不是去掉阻塞,而是:

  • 内联关键 CSS(首屏所需的最小样式),让首屏不依赖外部样式表
  • 非关键 CSS 异步加载
<!-- 关键 CSS 内联 -->
<style>/* 首屏必需样式 */</style>

<!-- 非关键 CSS 非阻塞加载 -->
<link rel="preload" href="/full.css" as="style" onload="this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/full.css"></noscript>

JS 用 defer / async 放行

属性 执行时机 顺序保证 适用
无(同步) 下载后立即执行,阻塞解析 按出现顺序 必须最先运行且极小的脚本
async 下载完立即执行 完全独立的脚本(统计、广告)
defer HTML 解析完成后、DOMContentLoaded 前 依赖 DOM 或彼此依赖的脚本
type="module" 默认等同 defer 现代模块化代码

默认选 deferasync 只给真正独立、无依赖的第三方脚本。

字体:非阻塞 + 防跳动

网页字体是 LCP 与 CLS 的双重风险源。三件事一起做:

<!-- 1. 提前建连 -->
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

<!-- 2. 非阻塞加载:先 preload,再用 media=print 骗过渲染阻塞,onload 后生效 -->
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Inter&display=swap">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter&display=swap"
      media="print" onload="this.media='all'">
/* 3. 用 size-adjust 让回退字体的度量接近目标字体,消除替换时的跳动 */
@font-face {
  font-family: 'Inter-fallback';
  src: local('Arial');
  size-adjust: 107%;
}

display=swap 保证文字立即可读(不会白屏等待),配合度量匹配可把 CLS 压到接近 0。

图片优化:收益最大的单项

图片通常占页面总字节的 50% 以上,是最容易拿到收益的地方。三个维度,按收益排序:

1. 尺寸(收益最大,最常被忽略)

把 3000px 宽的图塞进 300px 的容器,是纯粹的资源浪费。用 srcset + sizes 让浏览器按屏幕挑:

<img src="photo-800.jpg"
     srcset="photo-400.jpg 400w, photo-800.jpg 800w, photo-1600.jpg 1600w"
     sizes="(max-width: 600px) 100vw, 800px"
     width="800" height="600"
     alt="描述"
     loading="lazy" decoding="async">

需要按目标尺寸输出多个版本时,可用本站的 图片压缩图片格式转换,均在本地浏览器完成。

2. 格式(收益次之)

格式 相比 JPEG 透明 动画 浏览器支持 适用
AVIF 约小 50% 较新(Safari 16+) 优先,编码慢需离线处理
WebP 小 25–35% 广泛 稳妥的默认值
JPEG 基准 全部 照片的兜底
PNG 更大 全部 需无损/透明且不适合 SVG 时
SVG 极小 全部 图标、logo、简单图形

<picture> 让浏览器自己挑,不要 JS 嗅探:

<picture>
  <source type="image/avif" srcset="photo.avif">
  <source type="image/webp" srcset="photo.webp">
  <img src="photo.jpg" width="800" height="600" alt="描述">
</picture>

完整的格式特性对照见 图片格式速查表

3. 加载时机

  • 首屏图片不要懒加载(会拖慢 LCP),反而该 fetchpriority="high" 并 preload
  • 首屏之外的图片一律 loading="lazy"
  • decoding="async" 避免解码阻塞主线程
<!-- LCP 元素:优先加载 -->
<img src="hero.jpg" fetchpriority="high" decoding="async" width="1200" height="600" alt="主视觉">

<!-- 首屏外:懒加载 -->
<img src="item.jpg" loading="lazy" decoding="async" width="300" height="200" alt="列表项">

JS 与 CSS 体积

JS:分包优先于压缩

手段 说明 优先级
路由级分包 只加载当前页面需要的代码 最高
删除未使用依赖 用 bundle 分析器找体积占比异常的包
动态 import 重功能(编辑器、图表、PDF)按需加载
Tree Shaking 依赖 ESM 与正确的 sideEffects 声明
压缩混淆 收益已被 gzip/brotli 吃掉大半

一个经验判断:首屏 JS compressed 超过 300 KB 就该警惕。先看是不是把编辑器、图表库、PDF 解析器等重物打进了首屏,再谈压缩。

CSS:删比压重要

  • 移除未使用的 CSS(Tailwind/PurgeCSS 类工具)
  • 内联关键 CSS(见上文)
  • 格式化与压缩:可用本站的 CSS 格式化JS 格式化 做规范化,JSON 用 JSON 压缩

网络层

缓存策略

资源类型 建议策略 示例
带 hash 的静态资源 长期不可变缓存 Cache-Control: max-age=31536000, immutable
HTML 文档 每次校验 Cache-Control: no-cache + ETag
API 响应 短时或按业务定 Cache-Control: private, max-age=60
图片等媒体 中等时长 + 校验 Cache-Control: public, max-age=86400, must-revalidate

核心模式:文件名带内容 hash(app.a1b2c3.js),内容变了文件名就变,于是可以放心地设一年缓存;HTML 永不强缓存,保证发版立即生效。

压缩与协议

  • Brotli 优先,gzip 兜底:Brotli 通常比 gzip 再小 15–20%
  • HTTP/2 或 HTTP/3:多路复用,域名分片(sharding)在 HTTP/2 下不但无益反而有害
  • CDN:把静态资源推到离用户更近的边缘节点
  • 提前建连preconnect(关键第三方域)/ dns-prefetch(次要域)

CLS:六大诱因与修复

诱因 典型场景 修复
无尺寸媒体 <img> 没写宽高 width/height 或用 CSS aspect-ratio
动态插入内容 广告、通知条、懒加载区块 预留固定高度的容器,或用 min-height 占位
字体替换 FOUT/FOIT 造成文字重排 预加载字体 + size-adjust 度量匹配 + font-display: optional
动画触发布局 top/left/width/height 只动 transformopacity
骨架屏落差 骨架与实际内容尺寸不符 让骨架尺寸贴近真实内容
异步注入 UI Cookie 横幅、A/B 测试变体 用覆盖层(position: fixed)而非插入文档流

INP:把长任务拆开

INP 高的本质是主线程被长任务占住,导致交互无法及时响应。

手段 做法
拆分长任务 把超过 50ms 的任务切成小块,用 scheduler.yield()setTimeout 让出主线程
减少不必要 JS 首屏 JS 越少,执行与解析越快
延迟第三方脚本 统计、广告、客服插件按需或空闲时加载
避免大规模重排 批量读写 DOM,不要在循环里交替读写布局属性
用 Web Worker 把纯计算移出主线程
保持 DOM 精简 DOM 节点数越多,样式计算与布局越慢
// 把长循环切成可让出主线程的块
async function processAll(items) {
  for (let i = 0; i < items.length; i++) {
    process(items[i]);
    if (i % 50 === 0) await scheduler.yield();  // 让浏览器处理交互
  }
}

测量工具

工具 数据 最适合
GSC 核心网页指标报告 CrUX 真实用户 判断是否达标
PageSpeed Insights 两者都给 一站式看真实数据 + 实验室建议
Lighthouse(DevTools) 实验室 本地迭代、看具体机会项
WebPageTest 实验室,可调参数 瀑布图、多地点、弱网模拟
CrUX Dashboard / BigQuery 真实用户,可分段 长期趋势与细分分析

上线检查清单

  • [ ] LCP ≤ 2.5s、INP ≤ 200ms、CLS ≤ 0.1(看 CrUX 第 75 百分位,不是 Lighthouse 跑分)
  • [ ] 所有 <img> / <video> / <iframe> 都有 widthheight(或 aspect-ratio
  • [ ] LCP 元素未懒加载,且有 fetchpriority="high"
  • [ ] 首屏外图片全部 loading="lazy"
  • [ ] 图片使用 AVIF/WebP 并带 JPEG/PNG 回退
  • [ ] CSS 关键部分内联,非关键部分非阻塞加载
  • [ ] JS 默认 defer,第三方脚本 async 或延迟加载
  • [ ] 字体非阻塞加载且带 display=swap,回退字体做度量匹配
  • [ ] 静态资源带 hash + max-age=31536000, immutable
  • [ ] 已启用 Brotli 或 gzip
  • [ ] 首屏 JS(压缩后)控制在 300 KB 以内
  • [ ] 无超过 50ms 的长任务阻塞主线程

相关资源

图片处理:本站的 图片压缩图片格式转换图片尺寸调整 均在本地浏览器完成,不上传。格式特性对照见 图片格式速查表,缓存与压缩响应头见 HTTP 头速查表

广告

常见问题

Core Web Vitals 的三个指标分别衡量什么?达标线是多少?

**LCP(最大内容绘制)** 衡量加载速度,达标 ≤ 2.5 秒;**INP(交互到下次绘制)** 衡量交互响应,达标 ≤ 200 毫秒,它已于 2024 年 3 月正式取代 FID;**CLS(累积布局偏移)** 衡量视觉稳定性,达标 ≤ 0.1。三者取**第 75 百分位**(即四分之三的真实用户访问都要达标),且需同时满足才算通过。注意这是真实用户数据(CrUX),不是 Lighthouse 实验室跑分。

CLS 一直降不下来,最常见的原因是什么?

按出现频率排序:**① 图片/视频/iframe 没有写 width 或 height**(浏览器无法预留空间,加载后把内容顶下去,这是头号元凶);② 广告、嵌入内容、懒加载区块在渲染后动态插入;③ 网页字体加载导致文本重排(FOUT/FOIT);④ 在已有内容之上插入横幅或通知条;⑤ 动画使用了会触发布局的属性(如 top/left/width)。修复优先级:先给所有媒体元素补尺寸或用 `aspect-ratio`,再处理动态插入区域,字体用 `font-display: optional` 或预加载。

图片该用 WebP 还是 AVIF?

**优先 AVIF,回退 WebP,再回退 JPEG/PNG**。同一视觉质量下,AVIF 通常比 JPEG 小 50% 左右,WebP 小 25–35%。AVIF 的代价是编码更慢(离线批量处理没问题,不适合实时生成)且浏览器支持略晚(Safari 16+)。正确做法是用 `<picture>` 让浏览器自己挑,不要靠 JS 嗅探:`&lt;source type=&quot;image/avif&quot;&gt;` → `&lt;source type=&quot;image/webp&quot;&gt;` → `&lt;img&gt;` 兜底。**别忽略尺寸**:把 3000px 的图塞进 300px 的容器,格式再先进也没用,响应式 `srcset` 的收益通常比换格式更大。

Lighthouse 分数很高,为什么 CrUX 数据还是不达标?

因为两者衡量的东西不同。**Lighthouse 是实验室数据**:在固定网络与设备条件下跑一次,结果稳定可复现,适合定位问题;**CrUX 是真实用户数据**:覆盖实际设备性能、网络环境、用户交互路径,Google 排名用的也是它。常见落差来源:真实用户里大量中低端安卓机(CPU 慢 3–5 倍)、移动网络延迟高、用户会真正滚动和点击(触发 Lighthouse 不会执行的懒加载与交互)。**判达标只看 CrUX**,Lighthouse 用来找原因。

← 返回博客首页