Skip to content

Core Web Vitals 性能优化实战 ​

性能优化最容易陷入两个误区:一是只看实验室分数,上线后真实用户体验没有改善;二是对着 Lighthouse 报告逐条优化,却不清楚指标之间的因果关系。

Core Web Vitals(CWV)提供了一套以用户为中心的质量度量。2021 年 Google 将其纳入搜索排名信号后,它从"技术指标"变成了需要持续经营的业务指标。

三个核心指标 ​

指标衡量内容良好阈值说明
LCP最大内容绘制≤ 2.5s首屏主要内容何时可见
CLS累积布局偏移≤ 0.1页面元素是否发生意外跳动
INP交互到下一次绘制≤ 200ms用户操作后界面多久有反馈

INP 在 2024 年 3 月正式取代 FID。FID 只衡量"第一次交互的输入延迟",INP 则覆盖整个会话中的交互,包括输入延迟、事件处理和绘制延迟,更接近用户真实感受。

阈值按第 75 百分位计算。也就是说,至少要保证 75% 的访问达到"良好",平均值好看没有意义。

先建立度量 ​

优化之前先埋点。用 web-vitals 采集真实用户数据(RUM):

ts
import { onCLS, onINP, onLCP, onTTFB } from 'web-vitals'

function report(metric: {
  name: string
  value: number
  id: string
  rating: string
  navigationType: string
}) {
  const body = JSON.stringify({
    name: metric.name,
    value: metric.value,
    id: metric.id,
    rating: metric.rating,
    navigationType: metric.navigationType,
    url: location.href
  })

  navigator.sendBeacon('/api/rum', new Blob([body], { type: 'application/json' }))
}

onLCP(report)
onCLS(report)
onINP(report)
onTTFB(report)

注意几点:

  • 指标是分页面、分设备、分网络采集的。移动端 4G 与桌面宽带的问题完全不同。
  • sendBeacon 在页面卸载时也能可靠发送,适合性能上报。
  • 上报要采样。全量上报在高流量场景下成本很高,可以按用户或会话采样。
  • 同时记录页面类型、路由、版本号,否则无法定位是哪次发布导致回归。

实验室数据(Lighthouse、Performance 面板)用于复现和定位问题,真实用户数据用于判断优化是否有效。两者不能互相替代。

LCP:首屏主要内容何时可见 ​

LCP 由四个阶段组成:

text
TTFB(首字节)
  → 资源加载延迟
  → 资源加载时长
  → 元素渲染延迟

优化要按阶段拆解:

  1. 降低 TTFB:CDN、边缘缓存、服务端渲染、减少重定向和 DNS 查询。
  2. 提前发现资源:preload 关键图片和字体,避免被 CSS 或脚本延迟发现。
  3. 提高资源优先级:LCP 图片使用 fetchpriority="high"。
  4. 避免懒加载首屏图片:loading="lazy" 用在首屏会直接推迟 LCP。
  5. 减少渲染阻塞:内联关键 CSS,拆分非关键脚本。
html
<link rel="preload" as="image" href="/hero.avif" fetchpriority="high" />
<img
  src="/hero.avif"
  width="1200"
  height="630"
  fetchpriority="high"
  alt="产品首图"
/>

对于文本型 LCP(常见于内容站),重点转成字体和渲染阻塞:

css
@font-face {
  font-family: 'Display';
  src: url('/fonts/display.woff2') format('woff2');
  font-display: swap;
  unicode-range: U+0000-00FF;
}

font-display: swap 让文字先用后备字体显示,避免长时间不可见,但要注意字体切换可能带来 CLS。使用 size-adjust 或选择度量接近的后备字体可以缓解。

定位 LCP 元素:

js
new PerformanceObserver((list) => {
  const entries = list.getEntries()
  const last = entries.at(-1)
  if (last) {
    console.log('LCP element:', last.element)
    console.log('LCP time:', last.startTime)
  }
}).observe({ type: 'largest-contentful-paint', buffered: true })

拿到真实元素后,再判断是资源问题、渲染问题还是 TTFB 问题。

CLS:避免页面跳动 ​

CLS 的计算基于"影响区域 × 距离比例",主要来源:

  • 图片、视频没有设置尺寸。
  • 异步插入的广告、弹窗、通知。
  • 字体切换导致文本尺寸变化。
  • 首屏骨架屏与真实内容高度不一致。
  • 动画修改了布局属性。

最直接的修复是为所有媒体元素预留空间:

html
<img src="/cover.jpg" width="800" height="450" alt="" />
<iframe src="/embed" width="560" height="315" title="嵌入内容"></iframe>

容器高度不确定时,用 aspect-ratio:

css
.cover {
  aspect-ratio: 16 / 9;
  background: #e5e7eb;
}

动画只改 transform 和 opacity。如果必须改变尺寸,考虑用缩放代替高度变化,并在动画期间将其移出文档流。

INP:交互响应速度 ​

INP 衡量从用户输入到下一帧绘制的完整时间,差的交互通常来自:

  • 事件处理函数里有大量同步计算。
  • 渲染了大列表或复杂组件。
  • 第三方脚本占用主线程。
  • 输入时发起同步布局查询。

优化思路是减少主线程上的工作:

js
async function onSearchInput(event) {
  const keyword = event.target.value

  // 立即反馈,不阻塞输入
  showLoading()

  // 把非紧急工作让出主线程
  await new Promise((resolve) => setTimeout(resolve, 0))
  const result = await search(keyword)
  render(result)
}

更现代的做法是使用 scheduler.yield() 把长任务主动切分,或者用 startTransition(React)标记非紧急更新。Vue 中则可以把重计算移到 watch 的异步回调或 Worker 中。

对于长列表,优先做虚拟滚动;对于大量 DOM 更新,使用 DocumentFragment 批量插入;对于非关键脚本,使用 defer、async 或动态导入延后执行。

建立性能预算与回归防护 ​

优化不是一次性项目。把 CWV 纳入 CI,可以防止新代码把指标拖回去:

js
// lighthouserc.js
module.exports = {
  ci: {
    collect: {
      url: ['http://localhost:4173/'],
      numberOfRuns: 3
    },
    assert: {
      assertions: {
        'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
        'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
        'total-blocking-time': ['warn', { maxNumericValue: 200 }]
      }
    }
  }
}

Lighthouse 无法直接测 INP,常用 Total Blocking Time(TBT)作为实验室代理指标。线上则以 RUM 的 INP 为准。

常见坑 ​

  1. 只优化首页。核心转化路径可能是详情页、结算页,按页面类型分别建预算。
  2. 用平均值掩盖长尾。CWV 看第 75 百分位,平均值会掩盖低端设备和弱网用户的问题。
  3. 盲目预加载。预加载过多资源会抢占带宽,反而推迟关键资源。
  4. 第三方脚本失控。广告、客服、A/B 实验脚本常常是 INP 和 LCP 的主要来源,建立加载与执行预算。
  5. 忽略发布回归。性能问题通常不是一次引入的,而是每次发布多几十毫秒累积出来的。

小结 ​

CWV 优化的闭环是:采集真实数据 → 定位具体元素和交互 → 针对性优化 → 用 CI 和监控防止回归。脱离度量谈优化,很容易把时间花在用户感知不到的地方。

基于 VitePress 构建 · 持续更新中