Web Vitals 实战:把 LCP 从 4 秒优化到 1.2 秒
年初老板转来一封邮件:Google 宣布 5 月起 Core Web Vitals 正式纳入搜索排名因子。我们官网的 LCP 4.2 秒,红得发紫。于是有了这次为期两周的性能专项,最终 LCP 1.2s、CLS 0.02,全绿。
这篇记录完整的优化过程和方法论。
先搞懂三大指标
LCP(Largest Contentful Paint,最大内容绘制):视口内最大内容元素渲染出来的时间。衡量加载速度,及格线 2.5s。
FID(First Input Delay,首次输入延迟):用户第一次点击/按键到浏览器响应的时间。衡量交互响应,及格线 100ms。(后续版本会被 INP 取代,这个下篇再说)
CLS(Cumulative Layout Shift,累积布局偏移):页面元素意外挪位的程度。衡量视觉稳定性,及格线 0.1。
三个指标分别回答:快不快?点得动不动?稳不稳?
测量:先量化再优化
别一上来就瞎改。数据来源分两类:
实验室数据(本地可控环境):Lighthouse、WebPageTest 真实用户数据(RUM):Chrome UX Report、PageSpeed Insights 的"真实用户体验"部分
想在自己的监控里接 RUM 数据,用官方库:
import { getLCP, getFID, getCLS } from 'web-vitals'
getLCP((metric) => sendToAnalytics('LCP', metric.value))
getFID((metric) => sendToAnalytics('FID', metric.value))
getCLS((metric) => sendToAnalytics('CLS', metric.value))
LCP 优化:从 4.2s 到 1.2s
Lighthouse 报告告诉我们 LCP 元素是首页 Banner 大图。拆解它的加载时间线:
HTML 下载 → CSS/JS 阻塞渲染 → 图片被解析 → 图片下载 → LCP
每一段都是优化空间。
1. 消灭渲染阻塞
首屏 CSS 内联,其余异步加载;JS 全部加 defer:
<!-- 首屏关键 CSS 直接内联 -->
<style>/* critical css */</style>
<!-- 完整 CSS 异步:media 技巧,加载完再启用 -->
<link rel="stylesheet" href="/main.css" media="print" onload="this.media='all'">
<script src="/app.js" defer></script>
2. LCP 图片最高优先级
<!-- 预加载 LCP 图,让它跳过排队 -->
<link rel="preload" as="image" href="/banner.webp">
<img src="/banner.webp" alt="首页横幅" width="1920" height="600" fetchpriority="high">
fetchpriority="high" 明确告诉浏览器:这张图最重要,先下。
3. 图片本身瘦身
- PNG 转 WebP,体积从 800KB 降到 120KB(Quality 80 肉眼无差)
- 响应式 srcset,手机不下桌面大图
<img
srcset="/banner-640.webp 640w, /banner-1280.webp 1280w, /banner-1920.webp 1920w"
sizes="100vw"
src="/banner-1280.webp"
alt="首页横幅"
>
4. CDN + 缓存头
静态资源上 CDN,Cache-Control 拉满一年(文件名带 hash,不怕更新不了)。
四板斧下来,LCP 4.2s → 1.2s,大头是图片瘦身和内联 CSS。
CLS 优化:从 0.35 到 0.02
我们的 CLS 罪魁祸首有两个:
1. 图片没写宽高
浏览器在图片下载完成前不知道它占多大地方,先按 0 高度排版,图来了再把内容顶下去——一次大偏移。
<!-- 写了 width/height,浏览器会按宽高比预留位置 -->
<img src="/banner.webp" width="1920" height="600" alt="">
即使 CSS 里用 width: 100%; height: auto; 做响应式,HTML 属性上的宽高依然有"预留位置"的作用。CSS 等价写法 aspect-ratio: 16 / 9 也行。
2. 广告位/异步组件撑开布局
第三方脚本加载完往页面里塞内容,布局又跳一次。解法:给异步容器一个固定 min-height 占位:
.ad-slot {
min-height: 250px; /* 按广告常见高度预留 */
}
FID 优化:长任务拆分
FID 高的本质是主线程被长任务(Long Task)霸占,用户点击时浏览器顾不上。
我们首页引入了一个巨大的第三方图表库,加载 + 初始化一个 600ms 的长任务。优化:
// 1. 动态 import,图表组件用到再加载
const Chart = await import('./Chart.vue')
// 2. 非关键初始化推迟到空闲时间
requestIdleCallback(() => {
initAnalytics()
})
加上把打包的 vendor 拆小(webpack 的 splitChunks),主线程空闲时间多了,FID 从 180ms 降到 40ms。
小结
- LCP 管加载(≤2.5s)、FID 管交互(≤100ms)、CLS 管稳定(≤0.1)
- LCP 优化四板斧:内联关键 CSS、preload LCP 图、图片瘦身 WebP + srcset、CDN
- CLS 两大源头:图片没宽高、异步内容没占位
- FID 高就是长任务多:动态 import、代码分割、requestIdleCallback
- 先测量再优化,RUM 数据比实验室数据更真实
性能优化最爽的部分是:所有努力都能在 PageSpeed Insights 的绿色分数里看到回报。网站速度提升后那周,老板在周会上点名表扬——这波排名因子的东风,借得值。
看到这里了?如果这篇文章帮到过你
请我喝瓶可乐就好 🥤

评论0 条
还没有评论,来抢沙发~