Skip to main content

性能预算与门禁

性能优化做一次容易,保持住难。性能预算把「快」变成一个可被 CI 拦截的硬指标。

一、体积、请求数与核心指标​

预算要拆成两类:资源预算(JS/CSS 总体积、首屏关键资源数量与体积、图片与字体体积、第三方脚本数量)和指标预算(LCP、INP、CLS 的目标值与「需要改进」的阈值)。只定指标不定资源,优化无从下手;只定资源不定指标,优化可能没落到体验上。

还有一类容易被漏掉的是数量预算:首屏请求数、第三方脚本个数、字体文件数。这些单个看着不大,叠加起来才是首屏被拖慢的常态。三类一起定,才能把「快」拆成可度量的条目。

资源预算数量预算指标预算单个 JS / CSS / 图片的体积上限请求数、第三方脚本数、字体数LCP / INP / CLS 的目标值(真实用户 p75)
图:三类预算各管一段——只有指标预算会随代码漂移,所以门禁要盯住它

二、把预算接进流水线​

落地要区分「实验室数据」和「真实用户数据」。实验室数据在 CI 里跑:固定环境(同样的机型、网络节流档位)、对关键页面跑分、和基线对比,超过阈值就让构建失败。关键是多轮取中位数——单次跑分抖动很大,直接拿单次值当门禁,门禁会变成噪声,团队很快就开始忽略它。

产物体积门禁用 size-limit、bundlesize 一类工具卡压缩后体积;指标门禁用 Lighthouse CI 一类工具对关键路由跑分。两者职责不同,不要互相替代。门禁工具在 CI 里通常以「超阈值即非零退出」的方式工作,构建直接失败:

预算还要分级:首页、商详、结算这类关键路由该更紧,后台管理页可以宽松,不要所有页面一个标准。打包器本身也能卡体积——Webpack 的 performance.maxAssetSize / maxEntrypointSize、Vite 的 build.chunkSizeWarningLimit 都能在构建里直接告警或报错,和 CI 门禁互补。阈值别一上来就定死:先宽松、随技术债还清再逐步收紧,留一点余量,否则门禁会变成「永远红」。

# CI 里跑体积门禁,超阈值即失败
npx size-limit
# Lighthouse CI 思路(工具类别,不绑定具体产品)
ci:
collect:
url: ["http://localhost:3000/", "http://localhost:3000/list"]
settings: { preset: "desktop", throttling: "provided" }
assert:
assertions:
"categories:performance": ["warn", { "minScore": 0.9 }]
"largest-contentful-paint": ["error", { "maxNumericValue": 2500 }]
// 体积门禁思路(压缩后体积上限)
{ "size-limit": [{ "path": "dist/static/js/main-*.js", "limit": "170 KB" }] }
预算项类型说明
首屏 JS 体积资源压缩后上限,按路由分
第三方脚本数数量每个都要登记用途与体积
LCP / INP / CLS指标目标值与「需改进」阈值

三、第三方脚本的失控​

自己写的代码是可控的,第三方脚本不是:埋点、客服、AB 实验、广告像素,每接一个都可能带来几百 KB 与额外的主线程占用。管理办法只有两条:建立准入清单(谁能加、加什么、为什么必须),以及强制异步加载与延迟执行。性能退化追查到最后,通常是「上个季度接的那个脚本」。

异步加载之外,还要给超时降级:第三方脚本加载失败或超时时,不能阻塞首屏渲染,更不能让页面白屏。把「这个脚本挂了会怎样」当成设计项,而不是等到线上出问题才想。

四、超限时的处理流程​

超阈值时,先区分是新代码还是存量。新代码应该硬性卡住——合入即失败,逼着作者在提交前处理。存量问题则设递减目标,逐步收敛,而不是一刀切要求立刻达标,否则存量团队会直接去改门禁阈值来「通过」。

定位增量来自哪个依赖,用产物分析配「对比上次构建的体积 diff」最快。如果确实暂时降不下来,可以申请临时豁免,但豁免必须有到期时间——没有到期的豁免会变成永久漏洞。

预算要落成可执行的配置,口头约定守不住:

[
{ "path": "dist/app.*.js", "limit": "180 kB" },
{ "path": "dist/vendor.*.js", "limit": "220 kB" },
{ "path": "dist/*.css", "limit": "60 kB" }
]

五、让门禁不被忽略​

门禁长期红着,就会被所有人忽略,等于没有。两个做法:实验室数据与真实用户数据(分位数)分开看,前者用于卡合入、后者用于看趋势;阈值变动要留记录,否则没人知道为什么突然变严或变松。

把性能趋势做成可见的看板,而不是只在失败时才出现,团队才会把它当成日常指标,而不是障碍。

第三方脚本往往不在打包产物里,必须单独设一项才拦得住:

// 统计页面上所有第三方脚本的体积与耗时
const third = performance.getEntriesByType('resource')
.filter((r) => !r.name.startsWith(location.origin))

console.table(third.map((r) => ({
name: r.name.split('/')[2],
kb: +(r.transferSize / 1024).toFixed(1),
ms: +r.duration.toFixed(0),
})))

六、让门禁真正被执行的几个前提​

门禁只阻止变慢,存量问题要靠目标递减慢慢收敛,指望设个数字就自动变快是不现实的。

实验室数据达标也不等于用户快——真实用户数据(分位数)和实验室数据要分开看。

阈值更不是越严越好:长期红着的门禁会被所有人忽略,阈值要定在能守住的线上。

预算的作用是让退化变得可见,不是让性能优化自动发生。

把预算卡进 CI 可以直接用 size-limit,它支持按入口与时间维度设限,落地成本很低。