前端性能预算应该怎样制定和落地?
性能预算把包体、资源、关键指标和业务完成时间转成可检查的上限,但预算必须绑定真实设备、网络和页面场景,并通过趋势与用户体验一起判断。
简答
- 性能预算把“页面要快”写成特定场景下可检查的数字约束。
- 先定义用户任务和测量条件,预算分资源、运行时和体验三类。
- 阈值区分警告线和阻断线;包体预算不能替代真实体验指标。
- 预算按构建产物和版本计算,超出后先定位原因再处理。
团队常说“首屏不能太大”“页面要快”,但没有数字和场景,代码评审很难判断一次依赖升级是否值得。性能预算把目标写成可检查的约束:某类页面在指定设备和网络下,初始 JavaScript、关键图片、LCP 或业务任务耗时不能超过某个范围。
预算不是一个永远正确的全球数字,也不是只限制 gzip 后的 JS 大小。它必须回答:对谁、在什么场景、测量哪一个阶段、超出后谁负责处理。
先定义用户任务和测量条件
阅读文章、搜索题目和打开资源详情的关键路径不同。首页可能关注 LCP 和初始 JS;搜索页还要关注输入到结果可交互的时间;图片编辑器则可能更关心首次打开编辑器的延迟和内存峰值。
同一个页面在高端桌面热缓存和低端手机冷缓存上的结果不能直接混成一个预算。至少要记录视口、设备 CPU、网络限制、缓存状态、数据量、浏览器版本和是否登录。预算的条件不清楚,超出或通过都没有可比性。
可以把预算写成场景表:
| 场景 | 条件示例 | 关注指标 |
|---|---|---|
| 首页冷启动 | 移动设备、4G、冷缓存 | HTML、初始 JS、LCP、CLS |
| 首页关键交互 | 冷启动完成后操作菜单或筛选 | 交互延迟与 INP 诊断数据 |
| 题目阅读 | 直接打开深链接、正文较长 | 首屏正文出现、字体、交互可用时间 |
| 搜索筛选 | 真实题库数量、连续输入 | 输入响应、请求瀑布、结果可交互 |
这里的数字应来自真实用户和实验室基线,而不是复制别人的“200 KB 一定合格”。
预算至少分成几种类型
资源预算可以限制初始 JavaScript、CSS、字体、关键图片数量和总传输量;运行时预算可以限制长任务、主线程阻塞和内存峰值;体验预算可以限制 LCP、INP、CLS 或一项真实业务任务的完成时间。
这些预算相互影响。把一张大图压缩到更小,可能改善 LCP;把代码拆成更多 chunk,可能降低初始体积,却让首次打开低频功能增加请求等待。不能只看一个数字宣布整体优化成功。
{
"homepage": {
"initialJsGzipBytes": 180000,
"initialCssGzipBytes": 60000,
"lcpMsP75": 2500,
"clsP75": 0.1
},
"questionDetail": {
"initialJsGzipBytes": 160000,
"taskReadyMsP75": 3000
}
}
这类文件只是项目约定,不能靠 JSON 本身自动阻止发布。真正的检查脚本还要明确如何计算“初始 JS”、使用哪个构建清单、怎样处理 gzip/brotli、动态导入和第三方资源,并把条件写进报告。
例如入口静态依赖了 80 KB 的业务块与 100 KB 的共享块,两个分支都引用共享块时,应按同一资源去重后计算 180 KB,而不是重复算两次。动态导入的 300 KB 编辑器若确实首屏不请求,可以不计入初始 JS,但应计入“首次打开编辑器”的任务预算;如果页面启动时主动预加载或调用该动态导入,真实请求路径又可能把它纳入初始阶段。
initialJsGzipBytes 必须固定压缩算法与参数,它不是浏览器 Network 中所有传输字节的同义词。解压后脚本体积、HTTP 头、缓存命中以及 Brotli 编码都会造成差异。脚本执行成本还取决于内容和设备,不能按压缩字节比例直接推算毫秒数。
一次只加载页面、不执行交互的实验不能得到完整 INP 结论,缺少事件不能填写为 0。CI 可对确定的交互脚本建立实验室延迟预算;线上 P75 则需要真实交互样本和明确统计窗口。上面的预算 JSON 是教学配置,不代表一次构建就能直接计算出真实用户 P75。
预算阈值要区分警告和阻断
一次轻微超出不一定应该让所有开发无法合并。可以设置接近目标的警告线和不可接受的阻断线:依赖升级导致 2% 增长时给出可见报告,超过关键页面硬上限时阻止发布,或者要求明确的例外说明。
例外不能只是“这次需求很重要”。应记录增加了什么、换来了什么用户价值、影响哪个场景、是否有替代方案、何时重新评估。对临时第三方资源、实验分支和生成文件要有明确的纳入或排除规则,避免用排除配置把超标内容藏起来。
预算也要关注趋势。一次构建低于阈值,不代表连续十次小幅增长不会把页面拖慢。CI 可以保存每个场景的历史结果,按版本、页面和设备对比;分位数和样本量不足时要标记不稳定,不要把一次最好成绩当成事实。
包体预算不能替代真实体验指标
初始 JS 变小通常是好事,但如果把关键逻辑改成用户点击后才下载,首次操作可能变慢。压缩后的字节数也不等于解析、编译和执行成本;低端设备可能因为代码执行而出现长任务。
同样,LCP 通过不代表搜索输入流畅,CLS 通过不代表键盘焦点正确。至少要把资源预算、Core Web Vitals 和一个真实业务任务组合起来。RUM 用来观察真实设备分布,实验室测试用来控制变量定位原因,两者不能互相替代。
预算要与构建产物和发布版本绑定
构建工具生成的 chunk 名称会变化,不能在脚本里硬编码某一次哈希文件名。应读取 manifest 或构建统计,按入口和动态边界计算,并确认代码分割没有把同一个依赖重复打入多个关键块。
入口 HTML、压缩资源、字体和第三方脚本的缓存策略也会影响真实结果。预算检查的构建产物必须与最终部署产物一致;开发服务器、未压缩源码和本地热缓存不能作为生产预算证据。
如果页面使用预渲染或 SSR,HTML 到达时间、hydration 代码和后续交互要分别测量。静态 HTML 很快到达,不代表客户端已经能操作;只看构建产物大小也不能证明读者已经看到了有意义的内容。
超出预算后先定位原因再处理
先用构建分析确认是新依赖、重复模块、polyfill、图片、字体还是意外把低频功能静态导入。再结合 Performance 面板观察解析执行、长任务和布局成本。不要看到数字变大就机械删除功能或把所有依赖都改成懒加载。
如果增加来自必要的业务能力,可以比较预加载、服务端输出、压缩、缓存、虚拟化和更合适的算法;如果来自第三方脚本,应评估延迟加载、替代库和权限范围。性能优化仍要保持正确性、无障碍、安全和错误恢复。
一个可维护的性能预算应该让团队在开发、评审、CI 和线上监测中使用同一套定义。预算不是“页面永远不能变大”的口号,而是把用户体验目标转成可观察、可解释、可回顾的工程约束。