前端性能预算:从 Core Web Vitals 到发布门禁
性能优化不应该只在页面变慢后救火,更应该提前设定预算、持续测量并接入发布流程。本文介绍如何围绕 LCP、INP、CLS、资源体积和接口耗时建立一套可执行的前端性能预算。
很多团队做性能优化的方式是"感觉慢了再查"。
这种方式的问题是:等你感觉到慢,性能债通常已经堆了很久。图片越来越大、依赖越加越多、首屏接口越来越慢、组件初始化越来越重,每次改动都不明显,最后一起把用户体验拖下水。
性能预算的思路正好相反:先定义什么叫"不能再慢",再把指标接进开发和发布流程。
什么是性能预算
性能预算就是给页面性能设定一组明确上限。
例如:
首页 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 可以作为性能预算的核心。
最常用的三个指标是:
- LCP:最大内容绘制,衡量首屏主要内容出现得快不快
- INP:交互到下一次绘制,衡量页面响应用户操作是否及时
- CLS:累计布局偏移,衡量页面是否突然跳动
不要只看平均值。性能数据通常分布很不均匀,少数慢用户会被平均值掩盖。
更实用的做法是看分位数:
LCP p75 <= 2.5s
INP p75 <= 200ms
CLS p75 <= 0.1p75 的意思是 75% 的访问都应该满足这个目标。它比平均值更接近真实体验管理。
如果业务用户设备普遍较差,还可以额外关注 p90 或低端机分组。
不只预算 Web Vitals
Core Web Vitals 很重要,但它们不是全部。
前端还应该给这些东西设预算:
- JS 体积
- CSS 体积
- 图片体积
- 字体文件数量和体积
- 首屏请求数量
- 关键接口耗时
- 长任务数量
原因很简单: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))
}上报时至少带上这些维度:
- 页面路径
- 指标名称和值
- 设备类型
- 网络类型
- 是否命中缓存
- 版本号或 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}`)
}更成熟一点的做法是:
- 对主分支保存一份基线
- PR 中展示体积变化
- 超过固定阈值或增长百分比时阻断
- 允许维护者为必要增长写明原因
预算不是为了禁止所有增长,而是让增长变得可见。
LCP 优化要先找主元素
LCP 不是一个抽象数字,它对应页面上的某个元素。
常见 LCP 元素包括:
- 首屏大图
- 文章标题
- 商品主图
- 首屏 banner 文案
- 视频封面
优化 LCP 时,先确认 LCP 元素是谁。
如果 LCP 是图片,重点看:
- 图片是否过大
- 是否使用了合适格式
- 是否设置了明确尺寸
- 是否被懒加载误伤
- CDN 和缓存是否正常
如果 LCP 是文本,重点看:
- 服务端是否尽早返回 HTML
- 字体是否阻塞渲染
- CSS 是否过大
- 是否有客户端渲染导致内容迟迟出现
不要上来就压缩所有东西。先找到 LCP 对应元素,再处理它的关键路径。
INP 优化要看主线程
INP 变差,常见原因不是网络慢,而是主线程忙。
用户点击按钮后,浏览器需要等 JavaScript 执行、样式计算、布局和绘制。如果主线程正在处理一段很长的任务,交互反馈就会变慢。
常见问题包括:
- 一次性渲染过大的列表
- 输入时同步做复杂计算
- 事件回调里处理大量数据
- 第三方脚本占用主线程
- hydration 阶段执行太多初始化逻辑
优化方向通常是:
- 拆分长任务
- 大列表虚拟滚动
- 输入计算加 debounce 或放到 Web Worker
- 非关键脚本延后加载
- 减少首屏同步初始化
INP 不只属于"性能同学",它和组件写法、状态设计、依赖选择都有关系。
CLS 优化要给空间
CLS 高,说明页面在用户阅读或操作时发生了意外移动。
常见来源:
- 图片没有宽高
- 广告位或推荐位后插入
- 字体切换导致文本高度变化
- 异步内容加载后撑开布局
- 顶部提示条突然出现
最有效的策略是提前预留空间。
图片设置尺寸:
<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%,这些可以要求解释或修改。
第三阶段按页面等级设门禁。
核心页面严格,低流量页面宽松;新增页面严格,历史页面先给治理周期。
性能预算最终要成为团队共同维护的规则,不是某个人手里的红牌。
总结
性能优化最怕只靠临时救火。
一套可执行的性能预算至少包括:
- 用 Core Web Vitals 管结果
- 用资源体积和接口耗时管原因
- 用 RUM 看真实用户
- 用 CI 拦明显回退
- 用页面类型区分目标
- 用版本号定位是哪次发布造成变化
预算不是为了让页面永远不变大,而是让每一次变大都被看见、被讨论、被负责。
当性能从"上线后再看"变成"开发时就知道",优化才真正进入工程体系。