去年做了半年的性能优化专项,把站点的 CWV 指标从全红优化到全绿。分享一下三个指标各自的优化手段,都是实战验证过的。
先搞清楚三个指标是什么
| 指标 | 衡量什么 | 好的标准 | 差的标准 |
|---|---|---|---|
| LCP (Largest Contentful Paint) | 首屏最大元素加载完成的时间 | ≤ 2.5s | > 4s |
| INP (Interaction to Next Paint) | 用户交互到页面响应的延迟 | ≤ 200ms | > 500ms |
| CLS (Cumulative Layout Shift) | 页面视觉稳定性(抖动程度) | ≤ 0.1 | > 0.25 |
INP 是 2024 年 3 月正式替代 FID 的,如果你的资料里还在讲 FID,已经过时了。
LCP 优化
LCP 的核心瓶颈通常是四个环节之一:资源发现慢、下载慢、渲染慢、被阻塞。
1. 找到你的 LCP 元素
js
// 用 web-vitals 库
import { onLCP } from 'web-vitals'
onLCP((metric) => {
console.log('LCP:', metric.value, 'ms')
console.log('LCP element:', metric.entries[0]?.element)
})
大部分页面的 LCP 元素是首屏的大图、hero banner 或者主标题文字。
2. 图片类 LCP 优化
html
<!-- 给 LCP 图片加优先级 -->
<link rel="preload" as="image" href="/hero.webp">
<!-- 或者用 fetchpriority(更现代) -->
<img src="/hero.webp" fetchpriority="high" alt="...">
<!-- 其他非首屏图片降级 -->
<img src="/gallery-1.webp" loading="lazy" alt="...">
实操中最有效的一步:给 LCP 图片加 fetchpriority="high",光这一步就能让 LCP 快 500ms-1s,因为浏览器默认给所有图片一样的优先级,加了之后浏览器会优先下载这张图。
图片格式用 WebP/AVIF,体积能减 30-50%:
html
<picture>
<source srcset="/hero.avif" type="image/avif">
<source srcset="/hero.webp" type="image/webp">
<img src="/hero.jpg" alt="...">
</picture>
3. 服务端优化
- 开启 Brotli 压缩(比 gzip 再小 15-20%)
- 关键 CSS 内联到 HTML 里,非关键 CSS 异步加载
- SSR 或 SSG,至少让 LCP 元素的 HTML 结构在首字节里就带出来
html
<head>
<style>/* 关键 CSS,首屏渲染必须的样式 */</style>
<link rel="preload" as="style" href="/main.css" onload="this.rel='stylesheet'">
</head>
INP 优化
INP 衡量的是用户点击/输入后,浏览器多久能画出下一帧。瓶颈在主线程被 JS 占满了,没空响应。
1. 找到长任务
Chrome DevTools → Performance → 录制一次交互操作 → 看 Main 线程的火焰图,找超过 50ms 的任务块。
js
// 用 PerformanceObserver 监控
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 50) {
console.log('长任务:', entry.duration, 'ms', entry.startTime)
}
}
}).observe({ type: 'longtask', buffered: true })
2. 拆分长任务
最常见的长任务来源:大数据量的同步计算。
js
// 差:一次性处理 10000 条数据,主线程卡死 800ms
function processAll(items) {
items.forEach(item => heavyComputation(item))
}
// 好:用 requestIdleCallback 或 setTimeout 切片
async function processInChunks(items, chunkSize = 50) {
for (let i = 0; i < items.length; i += chunkSize) {
const chunk = items.slice(i, i + chunkSize)
chunk.forEach(item => heavyComputation(item))
// 让出主线程
await new Promise(resolve => setTimeout(resolve, 0))
}
}
3. 用 Web Worker 把计算移出主线程
js
// worker.js
self.onmessage = function(e) {
const result = heavyComputation(e.data)
self.postMessage(result)
}
// main.js
const worker = new Worker('/worker.js')
worker.postMessage(data)
worker.onmessage = (e) => {
updateUI(e.data) // 主线程只做 DOM 更新
}
4. 减少事件处理频率
js
// 差:scroll 事件每毫秒触发
window.addEventListener('scroll', () => {
updatePosition() // 每次都重排
})
// 好:节流
let ticking = false
window.addEventListener('scroll', () => {
if (!ticking) {
requestAnimationFrame(() => {
updatePosition()
ticking = false
})
ticking = true
}
})
CLS 优化
CLS 的元凶通常是:图片没尺寸、字体加载闪烁、动态插入内容把已有内容挤下去。
1. 图片给固定尺寸
html
<!-- 差:没有尺寸,加载后撑开页面 -->
<img src="/photo.jpg" alt="...">
<!-- 好:用 aspect-ratio 或 width/height -->
<img src="/photo.jpg" width="800" height="450" alt="..."
style="aspect-ratio: 16/9; width: 100%; height: auto;">
2. 字体加载避免 FOIT/FOUT
css
@font-face {
font-family: 'CustomFont';
src: url('/font.woff2') format('woff2');
font-display: swap; /* 先用系统字体,自定义字体加载完再替换 */
}
swap 会导致一次文字跳动(先显示系统字体,然后切换),如果对视觉稳定性要求高,用 optional(只在极快加载完成时使用)或者预加载字体文件:
html
<link rel="preload" as="font" href="/font.woff2" type="font/woff2" crossorigin>
3. 动态内容预留空间
广告位、推荐列表等动态插入的内容,给一个占位容器:
css
.ad-slot {
min-height: 250px; /* 预留广告位空间 */
}
监控
优化完之后不能不管了,加持续监控:
js
import { onLCP, onINP, onCLS } from 'web-vitals'
function sendToAnalytics(metric) {
navigator.sendBeacon('/analytics', JSON.stringify({
name: metric.name,
value: metric.value,
rating: metric.rating, // 'good' | 'needs-improvement' | 'poor'
url: location.href
}))
}
onLCP(sendToAnalytics)
onINP(sendToAnalytics)
onCLS(sendToAnalytics)
上报到自己的后端,或者直接接 Google Analytics 4 的 web-vitals 集成。
总结
- LCP :找到 LCP 元素 →
fetchpriority="high"→ 图片优化 → 关键 CSS 内联 → SSR - INP:找长任务 → 切片/Worker → 减少事件处理频率
- CLS:图片给尺寸 → 字体 preload + swap → 动态内容预留空间
三个指标里,CLS 最好改(加属性就行),LCP 中等,INP 最花时间(要改业务逻辑)。建议优化顺序:CLS → LCP → INP。