返回首页

前端性能预算:从 Core Web Vitals 到发布门禁

性能优化不应该只在页面变慢后救火,更应该提前设定预算、持续测量并接入发布流程。本文介绍如何围绕 LCP、INP、CLS、资源体积和接口耗时建立一套可执行的前端性能预算。

39 约 6 分钟 · 4379 字 前端

很多团队做性能优化的方式是"感觉慢了再查"。

这种方式的问题是:等你感觉到慢,性能债通常已经堆了很久。图片越来越大、依赖越加越多、首屏接口越来越慢、组件初始化越来越重,每次改动都不明显,最后一起把用户体验拖下水。

性能预算的思路正好相反:先定义什么叫"不能再慢",再把指标接进开发和发布流程。

什么是性能预算

性能预算就是给页面性能设定一组明确上限。

例如:

首页 LCP p75 <= 2.5s
交互 INP p75 <= 200ms
CLS p75 <= 0.1
首屏 JS gzip <= 180KB
首屏图片总量 <= 500KB
关键接口 p95 <= 800ms

预算不是漂亮口号,而是工程约束。

当某次改动让首屏 JS 增加 80KB,或者让 LCP 从 2.3s 变成 3.1s,团队需要能看到、能讨论、能决定是否允许上线。

先分清实验室指标和真实用户指标

性能指标大致分两类。

第一类是实验室指标,也就是在固定环境里跑出来的结果。常见工具包括 Lighthouse、WebPageTest、Playwright 脚本、CI 里的 bundle analyzer。

它的优点是可重复、适合做发布前检查。

第二类是真实用户指标,也就是 RUM。它来自真实用户的浏览器,会受到网络、设备、地区、缓存命中等因素影响。

它的优点是贴近真实体验,适合判断线上页面到底快不快。

两者都需要。

实验室指标像体检前的基础检查,能拦住明显问题。真实用户指标像长期健康记录,能告诉你用户实际感受到什么。

Core Web Vitals 该怎么用

Core Web Vitals 可以作为性能预算的核心。

最常用的三个指标是:

  1. LCP:最大内容绘制,衡量首屏主要内容出现得快不快
  2. INP:交互到下一次绘制,衡量页面响应用户操作是否及时
  3. CLS:累计布局偏移,衡量页面是否突然跳动

不要只看平均值。性能数据通常分布很不均匀,少数慢用户会被平均值掩盖。

更实用的做法是看分位数:

LCP p75 <= 2.5s
INP p75 <= 200ms
CLS p75 <= 0.1

p75 的意思是 75% 的访问都应该满足这个目标。它比平均值更接近真实体验管理。

如果业务用户设备普遍较差,还可以额外关注 p90 或低端机分组。

不只预算 Web Vitals

Core Web Vitals 很重要,但它们不是全部。

前端还应该给这些东西设预算:

  1. JS 体积
  2. CSS 体积
  3. 图片体积
  4. 字体文件数量和体积
  5. 首屏请求数量
  6. 关键接口耗时
  7. 长任务数量

原因很简单:Web Vitals 是结果指标,资源和接口是原因指标。

如果 LCP 变差,你还需要知道为什么变差。可能是首屏图太大,可能是接口慢,可能是 JS 阻塞主线程,也可能是字体加载导致文本迟迟不可见。

预算应该同时覆盖结果和原因。

一个可落地的预算表

对一个内容型站点,可以从这张表开始:

首页:
- LCP p75 <= 2.5s
- INP p75 <= 200ms
- CLS p75 <= 0.1
- 首屏 JS gzip <= 180KB
- 首屏 CSS gzip <= 60KB
- 首屏图片 <= 500KB
- 首屏请求数 <= 25

文章详情页:
- LCP p75 <= 2.8s
- INP p75 <= 200ms
- CLS p75 <= 0.1
- Markdown 内容接口 p95 <= 800ms
- 代码高亮相关 JS 不进入首屏主包

后台页面:
- 首次可交互 <= 4s
- 路由切换 p75 <= 500ms
- 表格筛选交互 INP p75 <= 200ms
- 单页初始 JS gzip <= 300KB

不同页面类型要有不同预算。

首页、详情页、后台工作台、活动页的用户目标不同,不能用一把尺子量到底。

如何采集真实用户指标

浏览器提供了 PerformanceObserver,可以采集很多性能数据。项目里也可以直接使用成熟的小库,把 Web Vitals 数据上报到自己的埋点系统。

伪代码大概是:

type MetricPayload = {
  name: string
  value: number
  rating: 'good' | 'needs-improvement' | 'poor'
  path: string
  device: string
}

function reportMetric(metric: MetricPayload) {
  navigator.sendBeacon('/api/metrics', JSON.stringify(metric))
}

上报时至少带上这些维度:

  1. 页面路径
  2. 指标名称和值
  3. 设备类型
  4. 网络类型
  5. 是否命中缓存
  6. 版本号或 commit sha

版本号很重要。没有版本维度,你很难判断是哪次发布让性能变差。

如何在 CI 里检查资源预算

真实用户指标适合线上监控,但它无法阻止坏代码合入。

资源体积预算可以更早放进 CI。

构建完成后输出 bundle 分析结果,然后检查关键包大小:

{
  "budgets": {
    "app.js": "180kb",
    "vendor.js": "220kb",
    "app.css": "60kb"
  }
}

检查逻辑可以很简单:

const budget = 180 * 1024
const actual = getGzipSize('.output/public/_nuxt/app.js')

if (actual > budget) {
  throw new Error(`app.js 超出预算: ${actual} > ${budget}`)
}

更成熟一点的做法是:

  1. 对主分支保存一份基线
  2. PR 中展示体积变化
  3. 超过固定阈值或增长百分比时阻断
  4. 允许维护者为必要增长写明原因

预算不是为了禁止所有增长,而是让增长变得可见。

LCP 优化要先找主元素

LCP 不是一个抽象数字,它对应页面上的某个元素。

常见 LCP 元素包括:

  1. 首屏大图
  2. 文章标题
  3. 商品主图
  4. 首屏 banner 文案
  5. 视频封面

优化 LCP 时,先确认 LCP 元素是谁。

如果 LCP 是图片,重点看:

  1. 图片是否过大
  2. 是否使用了合适格式
  3. 是否设置了明确尺寸
  4. 是否被懒加载误伤
  5. CDN 和缓存是否正常

如果 LCP 是文本,重点看:

  1. 服务端是否尽早返回 HTML
  2. 字体是否阻塞渲染
  3. CSS 是否过大
  4. 是否有客户端渲染导致内容迟迟出现

不要上来就压缩所有东西。先找到 LCP 对应元素,再处理它的关键路径。

INP 优化要看主线程

INP 变差,常见原因不是网络慢,而是主线程忙。

用户点击按钮后,浏览器需要等 JavaScript 执行、样式计算、布局和绘制。如果主线程正在处理一段很长的任务,交互反馈就会变慢。

常见问题包括:

  1. 一次性渲染过大的列表
  2. 输入时同步做复杂计算
  3. 事件回调里处理大量数据
  4. 第三方脚本占用主线程
  5. hydration 阶段执行太多初始化逻辑

优化方向通常是:

  1. 拆分长任务
  2. 大列表虚拟滚动
  3. 输入计算加 debounce 或放到 Web Worker
  4. 非关键脚本延后加载
  5. 减少首屏同步初始化

INP 不只属于"性能同学",它和组件写法、状态设计、依赖选择都有关系。

CLS 优化要给空间

CLS 高,说明页面在用户阅读或操作时发生了意外移动。

常见来源:

  1. 图片没有宽高
  2. 广告位或推荐位后插入
  3. 字体切换导致文本高度变化
  4. 异步内容加载后撑开布局
  5. 顶部提示条突然出现

最有效的策略是提前预留空间。

图片设置尺寸:

<img
  src="/cover.jpg"
  width="800"
  height="450"
  alt="文章封面"
>

异步卡片用骨架屏占位:

<template>
  <aside class="recommend-slot">
    <RecommendList v-if="data" :items="data" />
    <RecommendSkeleton v-else />
  </aside>
</template>

CSS 里也可以用 aspect-ratio 固定媒体比例:

.cover {
  aspect-ratio: 16 / 9;
  object-fit: cover;
}

CLS 的核心不是"别加载异步内容",而是"加载前就给它留好位置"。

发布门禁怎么定

性能预算进入发布流程后,最怕过度严格。

如果一开始就把所有指标设成一票否决,团队很快会绕过它。更好的方式是分阶段:

第一阶段只提示。

PR 里展示资源体积变化、Lighthouse 分数、关键指标趋势,但不阻断合并。

第二阶段阻断明显问题。

比如首屏 JS 增长超过 50KB、CLS 超过 0.25、LCP 回退超过 30%,这些可以要求解释或修改。

第三阶段按页面等级设门禁。

核心页面严格,低流量页面宽松;新增页面严格,历史页面先给治理周期。

性能预算最终要成为团队共同维护的规则,不是某个人手里的红牌。

总结

性能优化最怕只靠临时救火。

一套可执行的性能预算至少包括:

  1. 用 Core Web Vitals 管结果
  2. 用资源体积和接口耗时管原因
  3. 用 RUM 看真实用户
  4. 用 CI 拦明显回退
  5. 用页面类型区分目标
  6. 用版本号定位是哪次发布造成变化

预算不是为了让页面永远不变大,而是让每一次变大都被看见、被讨论、被负责。

当性能从"上线后再看"变成"开发时就知道",优化才真正进入工程体系。