性能优化最容易变成一次专项:页面慢了,团队集中几天压缩资源、加缓存、拆包,指标改善后便宣布完成。几个月后功能继续增长,同一个问题又以新的形式回来。

原因并不复杂。性能不是一个可以永久修复的缺陷,而是每次增加功能都会消耗的有限预算。如果没有边界和反馈,它必然被逐步透支。

从用户任务定义指标

技术指标很多:响应时间、首字节、首屏、可交互、帧率、内存、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 行一次渲染 服务端分页 + 保留筛选 首次可操作更早 翻页需要请求
图表更新 全量转换与绘制 按系列版本跳过未变部分 主线程长任务下降 增加版本契约

“代价”一列会阻止团队把优化写成纯收益。后续需求改变时,可以重新判断这笔交换是否仍然成立。

常见优化也有副作用。缓存会引入一致性问题,懒加载可能把等待推迟到用户点击时,预加载会浪费带宽,虚拟列表会增加滚动与可访问性复杂度。性能设计仍然是权衡,而不是技巧清单。

让退化更难发生

长期性能来自架构默认值。路由级拆包应默认开启,图片组件应提供尺寸与合理格式,数据层应避免重复请求,列表组件应对规模设定边界。开发者沿着默认路径工作时,结果就不应过度糟糕。

组件库也要包含性能契约。例如表格在多少数据量内直接渲染,超过后建议分页还是虚拟化;图表更新是全量替换还是增量更新;弹窗关闭后是否释放监听和大对象。

这些契约让性能从个人经验变成团队能力。新成员不需要踩过所有坑,系统本身就能给出约束。

优化也需要退出条件

性能工作很容易继续深入:再减少几十毫秒,再拆一段脚本,再调整一次缓存。每个优化开始前应定义目标值、验证环境和停止条件。达到用户任务需要的体验后,剩余资源可能更适合解决可靠性或功能问题。

还要记录没有采用的方案。某种预加载因为弱网浪费明显而放弃,某种服务端缓存因为用户状态难以隔离而暂缓,这些判断能避免团队几个月后重复相同实验。优化不是把所有指标推到极致,而是用有限投入消除当前最有价值的等待。

性能是一种产品判断

最快的页面是没有功能的页面。真实工作不是追求绝对速度,而是在功能、成本和体验之间找到可持续平衡。

某些等待可以被用户理解,例如导出大文件;某些等待会破坏信任,例如点击后没有任何反馈。优化优先级应该由任务价值和用户感知决定,而不是由最容易改善的指标决定。

经历服务端渲染与高流量页面的改造后,我越来越少把性能称为“前端优化”。它横跨接口、缓存、构建、运行时和产品流程。一个团队如果只在页面变慢时才关注性能,就像只在账户见底时才讨论预算。

真正成熟的性能体系,不是永远没有回退,而是回退能被尽早发现,成本能被看见,选择能被解释。

性能基线、PR 门禁、线上数据和预算回收的持续流程

图:预算进入每次变更后,性能才不再是上线前突击。

把预算写进流水线,而不是贴在文档里

我们给首屏 JavaScript、关键 CSS、LCP、接口 P95 和长任务设置基线。PR 只要让资源体积或实验室指标超过阈值,就必须写原因和回收计划;不能用一次总分掩盖单项退化。

预算按页面价值区分,低端设备和弱网单独跑。线上再用真实用户数据校正实验室门禁。这样性能不是上线前的一次冲刺,而是每次变更都能看到自己消耗了多少共同资源。