性能优化最容易变成一次专项:页面慢了,团队集中几天压缩资源、加缓存、拆包,指标改善后便宣布完成。几个月后功能继续增长,同一个问题又以新的形式回来。
原因并不复杂。性能不是一个可以永久修复的缺陷,而是每次增加功能都会消耗的有限预算。如果没有边界和反馈,它必然被逐步透支。
从用户任务定义指标
技术指标很多:响应时间、首字节、首屏、可交互、帧率、内存、QPS。它们只有映射到用户任务才有意义。
内容页的首要目标是尽快看到主体文字,运营后台更关心筛选后的表格何时可以操作,播放器页面则要关注首帧与控制响应。用同一指标评价所有页面,会奖励错误的优化。
我会先写出关键路径:用户从哪里进入,要完成什么,哪一步的等待会让任务中断。然后为路径选择少量核心指标,并同时观察中位数和长尾。平均值很容易掩盖网络差、设备旧或接口偶发慢的用户。
服务端同样如此。QPS 只是吞吐能力,必须和延迟、错误率、资源占用一起看。一个服务在低延迟时能承受多少稳定流量,比某次峰值更有参考价值。
建立可以重复的基线
没有基线,优化就只能依赖感受。开发机、测试环境和真实用户差异很大,单次测量又容易受缓存与网络波动影响。
我们将测量分成三层。开发阶段用浏览器工具定位网络、脚本与渲染瓶颈;持续集成里对关键页面运行固定环境测试,捕捉明显回退;上线后采集真实用户数据,观察设备与网络分布下的体验。
三层不能相互替代。实验室测试可重复,却不代表真实世界;真实数据足够诚实,却难以精确归因。把它们组合起来,才有机会从“发现变慢”走到“知道为什么变慢”。
每次发布应当保留版本标识,让指标变化可以对应到具体构建。否则折线上的异常只是一个时间点,无法回溯到代码。
预算要能阻止回退
性能预算可以作用于资源体积、关键请求数量、主线程长任务和页面指标。预算的数值不必一开始就完美,重要的是它能表达边界,并在越界时触发讨论。
例如入口脚本不能因为一次需求增长几百 KB;首屏不应串行等待多个彼此无关的接口;单个组件不应在滚动期间持续创建大对象。CI 可以检查资源体积,代码评审可以检查请求链路,监控可以发现真实指标回退。
预算不是机械阻止业务。越界可能合理,但必须明确交换:新增能力带来什么价值,能否延迟加载,是否需要删除旧成本。没有这种讨论,性能总会输给眼前功能。
预算最好进入仓库,而不是只写在性能文档里:
{
"routes": {
"/article/:slug": {
"initialJsKb": 180,
"criticalRequests": 4,
"lcpMs": 2500
},
"/dashboard": {
"initialJsKb": 260,
"longTasksOver50ms": 3,
"interactiveMs": 3000
}
}
}CI 可以稳定检查资源体积和请求数量,LCP 这类波动指标则使用固定环境跑多次并设回退区间,不因为单次抖动阻断提交。线上真实用户指标再验证预算是否代表真实体验。
一次合理越界需要在 PR 里写清交换。例如编辑器新增 35KB 解析器,但只在用户进入编辑模式后动态加载;入口包预算不变,编辑动作增加一次可解释等待。这比简单把预算从 180 改到 215 更诚实。
优化主要矛盾
性能工具会列出很多建议,逐条完成并不等于解决问题。应该先判断等待发生在哪一层。
如果首字节很慢,压缩前端图片帮助有限;如果主线程被大段脚本占用,升级服务器也不会改善交互;如果请求瀑布来自组件层层依赖,就需要调整数据获取结构,而不是再加一个加载动画。
一次有效优化应当能解释机制:删除了哪段阻塞,减少了多少数据,避免了几次重复计算。只报告“分数从 70 到 90”,很难判断收益能否持续。
我会用一张前后对照表结束优化,而不是只贴 Lighthouse 截图:
| 路径 | 优化前 | 改动 | 优化后 | 代价 |
|---|---|---|---|---|
| 首屏文章 | 串行等待推荐接口 | 主体与推荐并行,推荐延后渲染 | 主体不再被推荐阻塞 | 推荐区稍晚出现 |
| 运营表格 | 5000 行一次渲染 | 服务端分页 + 保留筛选 | 首次可操作更早 | 翻页需要请求 |
| 图表更新 | 全量转换与绘制 | 按系列版本跳过未变部分 | 主线程长任务下降 | 增加版本契约 |
“代价”一列会阻止团队把优化写成纯收益。后续需求改变时,可以重新判断这笔交换是否仍然成立。
常见优化也有副作用。缓存会引入一致性问题,懒加载可能把等待推迟到用户点击时,预加载会浪费带宽,虚拟列表会增加滚动与可访问性复杂度。性能设计仍然是权衡,而不是技巧清单。
让退化更难发生
长期性能来自架构默认值。路由级拆包应默认开启,图片组件应提供尺寸与合理格式,数据层应避免重复请求,列表组件应对规模设定边界。开发者沿着默认路径工作时,结果就不应过度糟糕。
组件库也要包含性能契约。例如表格在多少数据量内直接渲染,超过后建议分页还是虚拟化;图表更新是全量替换还是增量更新;弹窗关闭后是否释放监听和大对象。
这些契约让性能从个人经验变成团队能力。新成员不需要踩过所有坑,系统本身就能给出约束。
优化也需要退出条件
性能工作很容易继续深入:再减少几十毫秒,再拆一段脚本,再调整一次缓存。每个优化开始前应定义目标值、验证环境和停止条件。达到用户任务需要的体验后,剩余资源可能更适合解决可靠性或功能问题。
还要记录没有采用的方案。某种预加载因为弱网浪费明显而放弃,某种服务端缓存因为用户状态难以隔离而暂缓,这些判断能避免团队几个月后重复相同实验。优化不是把所有指标推到极致,而是用有限投入消除当前最有价值的等待。
性能是一种产品判断
最快的页面是没有功能的页面。真实工作不是追求绝对速度,而是在功能、成本和体验之间找到可持续平衡。
某些等待可以被用户理解,例如导出大文件;某些等待会破坏信任,例如点击后没有任何反馈。优化优先级应该由任务价值和用户感知决定,而不是由最容易改善的指标决定。
经历服务端渲染与高流量页面的改造后,我越来越少把性能称为“前端优化”。它横跨接口、缓存、构建、运行时和产品流程。一个团队如果只在页面变慢时才关注性能,就像只在账户见底时才讨论预算。
真正成熟的性能体系,不是永远没有回退,而是回退能被尽早发现,成本能被看见,选择能被解释。
图:预算进入每次变更后,性能才不再是上线前突击。
把预算写进流水线,而不是贴在文档里
我们给首屏 JavaScript、关键 CSS、LCP、接口 P95 和长任务设置基线。PR 只要让资源体积或实验室指标超过阈值,就必须写原因和回收计划;不能用一次总分掩盖单项退化。
预算按页面价值区分,低端设备和弱网单独跑。线上再用真实用户数据校正实验室门禁。这样性能不是上线前的一次冲刺,而是每次变更都能看到自己消耗了多少共同资源。