过度依赖 JavaScript 的开发模式,特别是基于 React 等框架的单页应用,与实现长期高性能目标存在根本冲突。这类方案因其庞大的依赖、脆弱的架构和复杂的调试过程,往往导致应用速度缓慢且难以维护。尽管它们承诺提升开发和用户体验,但实际效果常常不尽如人意。作为替代,应更多地采用以服务器为中心的架构,将计算压力从用户设备转移到服务器,从而为更广泛的用户提供更可靠、更高效的体验。
问题的根源
当前主流的 JS-heavy 开发方式,即向浏览器发送大量 JavaScript 并在关键路径上执行,存在一系列根深蒂固的问题,这些问题使得维持高性能变得异常困难。
依赖项代价高昂
JS 项目严重依赖 npm 包,但这带来了难以察觉的成本,主要体现在不断膨胀的打包体积 (bundle size)上。
- 许多包并非为小型化或模块化设计,导致即使只使用一小部分功能,也必须引入整个库。
- 依赖项会随着时间推移而变得越来越大。例如,
react-dom从 v18 升级到 v19 会使一个基础应用的体积增加 33%。 - 现有工具链很少会主动警示体积增加:
npm在安装或更新时不会告知包的大小。- 像
dependabot这样的工具会自动更新依赖,但不会警告新版本体积增大了 20%。 - 开发者往往因为安全修复或依赖关系被迫升级,无法停留在旧版本。
这些隐藏的体积增长日积月累,就像一个不断变重的锁链,拖慢了整个应用。
性能很容易变差
一个设计良好的系统应该让“做对事”比“做错事”更容易。然而,在性能方面,重度依赖 JS 的框架往往恰恰相反。
做错事更容易:
- 将状态放在顶层组件中全局共享,比按需加载更简单。
- 使用同步
import引入模块,比处理异步加载更直接。 - 编写一个每次都生成新数组的 Redux selector,比正确使用 memoization(缓存)更省事。
- 将所有 SVG 图标包装成 JSX 组件,比设置外部
<use>引用更方便。
教程和文档的误导:为了简化教学,许多官方文档和教程展示的是性能不佳的“简单”代码,这导致开发者从一开始就养成了坏习惯。
性能很难维持
由于系统本身的脆弱性,即使应用初始性能良好,也很难在持续开发中保持下去。性能优化成果非常容易被一次不经意的提交所破坏。
一次失误,前功尽弃:
- 你辛苦地将某个库的所有引用都改为异步加载,但只要有一个人在关键路径上添加了静态
import,之前的所有努力都将白费。 - 只要有一个写得不好的 Redux reducer,就可能在每次操作时都影响整个应用的性能。
- 你辛苦地将某个库的所有引用都改为异步加载,但只要有一个人在关键路径上添加了静态
依赖于纪律是不可靠的:在一个大型或快速迭代的团队中,依赖所有开发者的自觉和纪律来维持性能是不现实的。如果没有持续的监控,性能衰退几乎是必然的。
性能问题难以调试
尽管浏览器内置了强大的开发者工具,但许多 JS 框架,尤其是 React,却选择另起炉灶,开发出功能不全且与原生工具割裂的调试器。
- React DevTools 的局限:它无法与浏览器的性能分析器结合,导致你无法将组件的渲染行为与底层的 JS 执行、内存回收等信息关联起来,难以定位性能瓶颈的根本原因。
- 信息缺失和错误模糊:
- 服务器端渲染(SSR)的“注水”(hydration)失败时,错误信息通常毫无用处,比如
Did not expect server HTML to contain a <div> in <main>。 - 调试性能问题需要切换到特殊的“性能分析版本”(profiling build),而生产版本(production build)又缺少必要的调试符号。
- 服务器端渲染(SSR)的“注水”(hydration)失败时,错误信息通常毫无用处,比如
框架在底层平台之上增加了抽象层,这本身就会催生复杂的性能问题。然而,它们不仅没能帮助开发者应对这些问题,反而让调试变得比原生开发更加困难。
如何缓解问题
如果你不得不维护一个重度依赖 JS 的应用,可以采取以下措施来减缓性能衰退,但这需要巨大的投入。
准备阶段
- 统一认识:确保所有人都理解性能的重要性,它直接关系到转化率等业务指标。
- 定义预算:明确你的性能目标(如 Core Web Vitals),并作为开发过程中的硬性标准。
- 谨慎选择架构:在项目初期就评估是否需要服务器端渲染,并研究不同框架的性能优劣。
开发早期
- 代码分割:确保打包工具能按路由等方式生成多个代码块,并实现懒加载。
- 体积追踪:在持续集成(CI)中加入打包体积检查,当 PR 导致体积大幅增加时自动发出警告。
- 代码规范(Linting):设置规则,禁止导入已知有问题的库或大型模块的整体导入。
上线之后
- 真实用户监控(RUM):使用专业工具或自研系统,收集真实用户环境下的性能数据,这是衡量性能的黄金标准。
- 收集反馈:建立渠道,让支持团队能够将用户的性能抱怨传达给开发团队。
这套流程需要投入大量时间和精力来建立和维护。这就像是为了留在原地而拼命奔跑,而且你所处的位置可能本来就不是一个好地方。
真正的替代方案:服务器端工作
大多数性能问题的根源在于,在用户设备上执行 JavaScript 的成本极高。JS 是浏览器处理起来最“重”的资源,它不仅解析和编译开销大,而且大部分时间运行在单核上。
一个显而易见的替代方案是将大部分工作转移到服务器上。
服务器中心架构向浏览器提供的是现成的页面内容,而不是一份“菜谱”和一些“食材”,让浏览器自己费力地去“烹饪”。
警惕“伪”服务器方案
一些 JS 架构虽然引入了服务器端渲染(SSR),但它更像是一个创可贴。例如,整体注水(monolithic hydration)方案虽然能让页面更快可见,但浏览器仍然需要下载、解析和执行数兆字节的 JS,导致页面在可见和可交互之间存在巨大的延迟。
真正的服务器中心模型
真正的替代方案是采用服务器中心编程模型,大部分代码永远不会离开服务器。
- 优势明显:服务器环境更可预测、更易于扩展,性能调试也更简单。你无需担心代码体积,也无需消耗用户设备的电量和流量来运行你的应用代码。
- 可靠的架构:
- 全页面导航:这是 Web 的基础架构,经过验证,在配合 CDN 时性能表现出色。JS 仅作为点缀,用于增强交互。
- HTML 局部替换:像
turbolinks或原生的 View Transitions API,通过服务器渲染的 HTML 片段更新页面,体验流畅。 - HTML 增强:像
htmx这样的技术,用很少的 JS 为静态 HTML 增加动态功能。
当然,并非所有应用都适合这种模式,例如高度交互的在线编辑器。但即便必须在客户端渲染,也有比 React 更轻量、更高效的框架可供选择。
让我们改变做事的方式
重度依赖 JS 的应用在性能上起点很低,并会随时间推移而恶化。为了维持基本性能,需要付出巨大的、不可持续的努力。
我们应该停止盲目地为所有项目都选择 JS 框架,停止基于我们最熟悉的技术栈来做架构决策。在启动一个新项目前,我们应该先问自己:
- 客户端渲染真的有帮助吗?用户是否仍需等待 API 调用返回数据?
- 用户会在应用内进行足够多的操作,以抵消漫长的初始加载时间吗?
- 我们占用用户设备的资源来完成本可以在自己服务器上完成的工作,用户真的从中受益了吗?
我们亏欠用户,应该做得更好。因为我们现在的构建方式,往往是我们自己想要的,而不是用户真正需要的。