5.6 前端工程
概述与动机
前端工程是构建软件面向客户端的那一层的学科:这层代码运行在浏览器或设备中,把设计、内容和数据转化为一个可用的界面。它涵盖了框架与架构的选择、渲染策略、状态管理、性能,以及在现实世界中形形色色的浏览器、设备和网络条件下的弹性表现。前端正是所有上游工作(用户体验、设计、内容、无障碍、国际化)能否成功抵达用户手中,或者半途瓦解的地方。
对于大型团队而言,前端有其独特的挑战,因为它暴露在一个组织自身无法掌控的环境之中。用户的浏览器、设备、网络连接和设置千差万别,而这个平台(也就是 Web)又在持续演进。在规模化之后,架构选择的影响会不断叠加。今天选定的一个框架,会在未来数年内制约招聘、性能和可维护性,而成千上万个关于打包体积和渲染方式的细小决定,加在一起就构成了用户实际获得的体验。共享的标准、组件库、性能预算和架构模式,正是防止众多独立团队合力产出一个缓慢、不一致、脆弱的整体的关键所在。
企业和政府场景的相关性尤为突出。企业维护着长期存在的应用,在这些应用中,框架的长期存续能力和可维护性比新颖程度更重要,而且必须有众多团队能够相互协作。政府服务面向全体公众,包括那些使用旧设备、网速慢或按流量计费的连接,以及依赖辅助技术的人群。这使得性能、渐进增强和弹性,不再是可有可无的锦上添花,而是决定一项服务是能够惠及所有人、还是会将最弱势的群体排除在外的关键差别。一项只能在配备高速网络连接的最新款手机上运行的政府服务,是没有履行其使命的。
关键原则
- 前端运行在一个你无法掌控的环境中;应针对多变性和失败来设计。
- 为长期存续的系统选择枯燥、耐久的技术;以可维护性和可招聘性为优化目标。
- 性能是一项功能特性,对许多用户而言,更是能否访问服务的先决条件。
- 渐进增强:先交付一个可用的核心体验,再逐层叠加增强功能。
- 发送更少的代码;最快、最可靠的代码,就是你不发送出去的代码。
- 让渲染策略匹配内容类型和用户需求,而不是匹配潮流。
- 弹性:当出现问题时,界面应当优雅降级,而不是直接崩溃。
- 标准和平台特性比框架更长寿;应依靠平台本身。
建议
为长期存续能力和契合度选择框架,而不是为了追逐热点
选择前端技术时应基于问题本身、团队情况、维护周期和招聘市场,而不是当下流行什么。对于长期存续的企业和政府系统,应优先选择成熟、有良好支持、拥有庞大人才储备、发布节奏稳定、升级路径清晰的技术。要权衡框架频繁更迭所带来的总成本:重写既昂贵又有风险。优先选择依靠 Web 标准 的做法,这样即便框架更替,你的投入依然能够保留下来;并把与特定框架相关的代码隔离在明确的边界之内,让应用不至于被单一库的生命周期所挟持。
让渲染策略匹配实际需求
主要的渲染策略各自适合不同的内容。服务端渲染(SSR)能带来快速的首次绘制和良好的 SEO(搜索引擎优化),并且在没有客户端 JavaScript 的情况下也能工作,适合内容密集、面向公众的页面。静态站点生成(SSG)在构建时预先渲染,以获得最快的速度和最好的可缓存性,非常适合不常变化的内容。客户端渲染(CSR)适合登录后那种高度交互、类似应用的体验。流式渲染和渐进式水合会分批发送并激活页面,让用户更早看到并使用内容。许多大型系统会按路由混用这些策略,而不是全局统一采用一种。要有意识地管理状态:让服务端状态、URL 状态和本地 UI 状态各自分开,避免把一切都过度集中到一个庞大的全局存储中。
把性能当作一项有预算、可衡量的纪律来对待
采用性能预算(对打包体积、请求数量和关键指标设定明确的上限),并在 CI 中强制执行,让性能倒退直接导致构建失败。使用来自真实设备和真实网络的真实用户监控(而不仅仅是在高性能机器上做的实验室测试),来追踪核心网页指标(Core Web Vitals,涵盖加载速度、交互性和视觉稳定性)。大力削减 JavaScript:进行代码拆分和懒加载,让用户只下载某个视图实际需要的内容,推迟非关键性的工作,并优先使用平台自身能力而非重量级的库。优化图片和字体,有效利用缓存,并在具有代表性的低端设备和慢速网络上进行测量。
以渐进增强和弹性为原则来构建
从一个使用语义化 HTML、几乎不依赖或完全不依赖 JavaScript 也能工作的基线开始,再针对能力更强的客户端逐层增强。这能确保在脚本加载失败、设备老旧或网络不稳定()这是常见的现实情况,而不是边缘案例()的情况下,核心任务依然可以完成。要优雅地处理错误:为加载中、空状态、出错和离线等情况展示有用的界面状态,而不是一片空白屏幕或无休止的加载动画。对于人们依赖的服务,可以考虑采用离线优先的技术,让应用在网络间歇性中断的情况下依然可用,并在网络恢复后进行同步。
确保跨浏览器、跨设备与辅助技术的兼容性
依据真实的分析数据(而不是团队自己使用的机器),在用户实际使用的浏览器、设备和辅助技术上进行测试。使用渐进增强和特性检测,而不是假设最新的平台特性在所有地方都可用。进行响应式构建(参见设计系统章节),让同一套代码库能够服务从手机到桌面的各种设备。从一开始就把无障碍和国际化融入前端架构,而不是留到后期再补做。
把前端当作共享基础设施来治理
提供共享的组件库、代码检查、格式化和构建工具,让各团队保持一致并提高效率。制定架构指南(如何组织应用结构、管理状态、拆分打包体积),并在 CI 中强制执行性能预算。对于非常庞大的前端项目,可以考虑采用模块化或微前端架构,让各团队能够独立部署,但要仔细权衡由此增加的复杂性和性能代价,因为它们并非没有成本。
权衡:优点与缺点
| 决策 | 优点 | 缺点 |
|---|---|---|
| 流行且成熟的框架 | 人才储备庞大、稳定、有支持 | 可能带有历史包袱;采用最新特性较慢 |
| 最新的框架 | 现代特性、性能提升 | 更迭风险、人才储备小、长期存续能力不确定 |
| SSR / SSG | 首次绘制快、SEO 好、无需 JS 也能工作 | 服务端或构建复杂度高,缓存具有挑战性 |
| CSR(单页应用) | 交互丰富、体验类似原生应用 | 首次加载慢、依赖 JS、SEO 与弹性受损 |
| 重度客户端 JavaScript | 功能丰富 | 在低端设备上性能差,脆弱 |
| 渐进增强 | 具备弹性、包容性好,处处可用 | 需要更多设计工作来定义一个可用的基线 |
| 微前端 | 团队可独立部署,具备扩展性 | 复杂性高、依赖重复、存在性能开销 |
反复出现的权衡在于丰富度与开发便利性,同人群覆盖面、性能和弹性之间的对比。重度客户端方案在高性能机器上构建和演示都很惬意,但它们会把使用弱设备和弱网络的用户排除在外。对于企业、尤其是政府受众,应把天平向性能、渐进增强和耐久性倾斜,因为排除用户的代价很高,而且往往是没有商量余地的。
与团队讨论的问题
我们如何隔离与特定框架相关的代码,使应用不至于被某一个库的生命周期所挟持? 对于长期存续的企业和政府系统而言,框架更迭是最大的一项可以避免的开支:重写既昂贵又有风险,而今天流行的库会在未来数年内制约招聘和维护。依靠 Web 标准,并把与特定框架相关的代码放在明确的边界之后,意味着你的业务逻辑和内容能够在下一次框架更替中存活下来。要确定这些边界具体在哪里,以及一位新工程师是否能够分辨出哪些是平台代码、哪些是框架代码。带上你们上一次框架迁移实际花费的成本估算,或者即将到来的那一次预计会花费的成本。如果你的核心逻辑与某个库的 API 焊死在一起,那么在为这个框架选择辩护之前,先给这种耦合定个价。
我们是按路由匹配渲染策略,还是在整个产品上强行统一使用一种策略? 服务端渲染能带来快速的首次绘制,并且在无需客户端 JavaScript 的情况下也能为公开内容服务;静态生成能为不常变化的页面把速度提升到极致;客户端渲染适合登录后那种交互式、类似应用的界面。在全局层面强行统一一种策略,要么会用沉重的 JavaScript 拖慢公开页面,要么会给一个简单的内容页面过度设计。这对政府而言是一个关乎人群覆盖面的问题,因为一项只有在加载完一个庞大的打包文件之后才能工作的服务,会把使用旧设备和慢速网络的用户排除在外。带上你们的关键路由列表,为每一个标注出它今天实际使用的策略。如果一个面向公众的页面需要 JavaScript 才能显示其内容,要判断这是一个刻意的选择,还是一次意外。
我们的状态管理有多讲纪律,我们是否把一切都过度集中到了一个庞大的全局存储中? 让服务端状态、URL 状态和本地 UI 状态各自分开,能够防止那种让大型前端变得缓慢而脆弱的耦合和重复渲染风暴,然而一个诱人的默认做法却是把一切都塞进一个全局存储里。这在规模化之后会不断加剧,因为众多团队共同触碰同一个共享存储,会制造出隐藏的依赖关系和不可预测的性能表现。就每一类状态应当归属何处、以及什么不应当放入全局存储达成一致。带上一个渲染次数超出应有水平的组件,并追查其原因。如果答案是一个臃肿的中央存储,就要在这种耦合固化之前确定好边界。
我们的性能预算是什么,它们是否会在 CI 中导致构建失败,它们是否是在用户实际使用的设备上测量的? 一个没人强制执行的预算只是一个愿望,而一个只在团队自己的高性能笔记本电脑上测量的预算,描述的是一个并不存在的用户。对于大型组织而言,预算是唯一一种能够在数十个团队不断为一个共享界面添加功能的情况下,把打包体积和核心网页指标控制在可控范围内的机制,因为没有任何一位评审者能够凭肉眼捕捉到每一次性能倒退。与之相互竞争的压力是交付速度:因为几千字节的差异就导致构建硬性失败,在你算清楚这能避免多少用户流失之前,会显得像是在设置障碍。带上你们目前的预算、来自低端设备和慢速网络的真实用户监控数据,以及一份哪些发布版本曾让性能倒退溜过检查的清单。在政府场景中,由于使命是服务全体公众,包括使用旧手机和按流量计费数据的人群,应把预算与你们最慢的那十分之一用户、而不是中位数用户挂钩,并让 CI 门禁没有商量余地。
我们哪些服务必须在没有客户端 JavaScript 的情况下依然可用,我们是否真的测试过这条路径? 渐进增强说起来容易,却也容易在不知不觉间被破坏,因为增强后的路径是开发者每天都在使用的那条,而基线路径则会在无人测试的情况下悄悄腐坏。在规模化之后,有意识地决定这一点变得尤为重要,因为众多团队共同面向同一个平台发布内容,除非有一个共享标准明确规定,否则每个团队都会各自假设脚本总是能够加载成功,而单一一个硬性依赖就可能让任何一个打包文件加载失败的用户无法完成核心任务。这里的权衡是真实存在的:一个可用的无 JavaScript 基线需要投入设计精力,并会限制你构建交互功能的方式。带上你们的关键用户旅程、一个在脚本被禁用或加载失败情况下加载每一条旅程的测试,以及脚本在实际场景中真正加载失败的频率证据。对于一项公共服务而言,一个在某个脚本超时后就崩溃的福利或税务表单,不是一种体验降级,而是意味着一位公民无法完成一项法定义务,因此应把这个基线视为一项合规要求,而不是一个锦上添花的选项。
微前端在什么时候才真正值得其带来的复杂性,又由谁在团队伸手采用它之前做出决定? 团队独立部署确实很有吸引力,但微前端会带来分布式系统的复杂性、重复的依赖,以及一项由用户以更慢的加载速度来承担的性能税。如果没有一个共享的决策点,雄心勃勃的团队往往会出于组织上的便利,在规模远未证明这种成本合理之前就采用微前端,而整个产品都会继承这份额外开销。与之相互竞争的考量是自治性:在一个共享代码库上发布的团队会相互阻塞,而在真正的规模之下,这种耦合本身就是一个代价高昂的问题。带上触碰这个界面的团队数量、你们今天实际经历的部署争用情况,以及一次拆分会带来多少载荷重复的量化估算。对于企业和政府平台而言,由于架构决策会在数年间约束众多团队、并且必须经得起审计和交接,应要求一个明确的、有文档记录的阈值,以及一位批准这一举措的负责人,而不是让每个团队各自孤立地做决定。
行业视角
初创企业。 当每一次注册都至关重要时,速度和覆盖面同样重要,因此要抵制为公开页面构建重度单页应用的冲动。对你们的营销页面和注册流程进行服务端渲染,让它们在早期客户所使用的中端手机和不稳定的数据网络上也能快速加载,并把客户端交互性保留给登录后的应用部分。在 CI 中设定一个简单的打包体积预算,防止一个不经意引入的依赖悄悄把页面体积撑大,并依靠 Web 标准,让一个小型代码库在你们不断招聘的过程中依然保持可维护。
小型企业。 由于没有专职的前端专家、预算也紧张,应优先选择一个有良好支持的主流框架,或者一个托管式的网站构建工具,而不是任何定制化的方案,这样你招聘时就能从一个庞大的人才库中选人,并且是在购买维护服务,而不是自己组建团队来维护。把这个选择定位为耐久性问题:最便宜的选项,就是那个不会逼你在两年后重写的选项。坚持要求默认就具备快速、移动端友好的页面和无障碍的标记,因为一个缓慢或损坏的结账流程,会让你流失掉你承受不起损失的客户。
企业。 这里的问题是跨众多团队的一致性:一个共享组件库、一套约定的架构模式、代码检查和构建工具,以及在 CI 中强制执行的性能预算,确保没有哪个团队能够在不知不觉中让整体性能倒退。为了长期存续能力和可招聘性、而不是新颖程度来选择框架,把与特定框架相关的代码隔离在边界之后以挺过下一次迁移,并按界面来匹配渲染策略。把前端当作共享基础设施来管理,配备真实用户监控、治理机制,以及一份可供审计的、记录每一次架构选择理由的档案。
政府。 你们服务的是全体公众,包括使用旧设备、网速慢或按流量计费的连接,以及依赖辅助技术的人群,因此渐进增强和性能是义务,而不是锦上添花。把一个可用的无 JavaScript 基线定为面向公民服务的硬性规则,按最慢的用户群体、而不是中位数用户来设定页面预算,并确保即使脚本失败,核心任务依然能够完成。采购和透明度方面的要求也适用:优先选择依靠标准、能够避免被单一供应商锁定的耐久技术,在合同中记录无障碍和性能要求,并且能够证明这项服务对最弱势的用户同样有效,而不仅仅是在演示设备上有效。
示例
初创企业。 一家种子轮阶段的初创公司曾一度打算把其营销网站和注册流程构建成一个重度单页应用,但他们的目标客户往往是在信号不稳定的移动数据网络上使用中端手机的购物者。两位创始人转而对公开页面进行了服务端渲染,让它们能够快速加载,并在任何 JavaScript 运行之前就能正常工作,同时把客户端交互性保留给登录后的应用部分。他们在 CI 中设定了一个简单的打包体积预算,防止一个不经意引入的依赖悄悄把页面体积撑大。这种精简、快速的首次加载明显提升了注册转化率,而依靠 Web 标准也让他们在不断招聘的过程中,让这个小型代码库依然保持易于维护。
企业。 一家金融服务公司通过统一采用一个成熟框架、一个共享组件库,并在 CI 中强制执行性能预算,对其庞杂的一系列内部和客户应用进行了现代化改造。渲染策略是按界面分别选择的:面向公开营销和内容的页面采用可缓存的服务端渲染,而登录后用于交互式仪表盘的应用则采用客户端渲染。打包预算和真实用户监控在发布之前就捕捉到了性能倒退,让公司众多团队之间的加载时间保持较快水平,并降低了此前曾迫使他们进行代价高昂的重写的框架更迭风险。
政府。 一个国家级数字服务团队在构建面向公民的服务时,把渐进增强定为一条硬性规则:每一项服务都首先依靠语义化 HTML 和服务端渲染来工作,JavaScript 只起增强作用。这确保了该服务能够在旧手机、网速缓慢的农村地区网络,以及辅助技术上正常运行()这些都是政府不能将其排除在外的群体。性能预算让页面在低端设备上保持轻量和快速,而优雅降级则意味着一个失败的脚本永远不会阻止某人完成一份福利申请。最终结果是一项快速、有弹性、无障碍、且能被全体公众使用的服务。
商业理由:动机、投资回报率与总拥有成本
前端工程方面的选择会直接影响收入、覆盖面和成本。性能与转化率、参与度和任务完成率直接挂钩。更快的体验在数据上明显优于较慢的体验,而对于使用弱设备的用户而言,性能就是”使用这项服务”和”放弃这项服务”之间的分界线。渐进增强和跨设备支持能够扩大可触达的受众范围,这对政府而言是一项使命,对企业而言则是市场份额。稳健的框架和架构选择,能够降低重写的频率和成本()而重写正是前端工程中最大的一项可以避免的开支。
在总拥有成本方面,采用这些做法的成本体现在坚持性能预算和测试的纪律上、渐进增强所需投入的努力上,以及对共享工具和组件库的投资上。而不采用这些做法的代价,则体现在拖慢体验从而流失用户和收入、把低端设备和使用辅助技术的用户排除在外(在政府场景中还会带来法律风险敞口)、在实际使用中崩溃的脆弱应用,以及因追逐潮流而带来的代价高昂的框架更迭和重写。前端方面的问题往往以分散的用户流失和客服负担的形式浮现出来,而不是体现为账目上的一条单独明细,因此很容易被投入不足。
要向领导层证明其合理性,就把核心网页指标和加载时间与转化和任务完成漏斗联系起来,量化因重度客户端方案而被排除在外的用户数量,并把过去或即将到来的重写成本,同一个耐久、依靠标准的架构所带来的稳定性进行比较定价。把性能预算和渐进增强定位为降低风险和扩大覆盖面的手段。
反模式与陷阱
- 追逐框架热点: 为了赶上最新的库而重写,招致更迭成本却没有给用户带来任何好处。
- 仅依赖 JavaScript 才能使用的体验: 在一个庞大的打包文件加载并运行完毕之前,什么都用不了,把许多用户排除在外。
- 只在高性能设备上测试: 团队自己使用的旗舰笔记本电脑掩盖了真实的用户体验。
- 忽视打包体积: 依赖不断无节制地增长,直到页面在所有地方都变得缓慢。
- 没有性能预算: 性能倒退在一个又一个发布版本中悄悄累积。
- 空白屏幕式的失败: 没有加载中、空状态、出错或离线等状态;一次失败的请求就会破坏整个页面。
- 过度集中的全局状态: 一切都放在一个存储里,造成耦合和重复渲染风暴。
- 过早采用微前端: 引入了分布式系统的复杂性和重复的载荷,却没有相应的规模来证明其合理性。
- 在架构中忽视无障碍和国际化: 到后期再以高昂的代价把它们硬塞进去。
成熟度模型
第 1 级:启动。 各团队各自临时构建前端,没有共享标准。客户端代码沉重,没有性能预算,只在团队自己的设备上测试过。框架的选择凭个人喜好或潮流,一个失败的脚本可能会让用户面对一片空白屏幕。
第 2 级:发展。 部分团队采用了共享工具和组件库,但整个组织的实践水平参差不齐。性能只是偶尔被测量,而不是被预算或强制执行。渲染策略往往不管内容类型如何都统一采用一种,跨设备测试也有限且是手动进行的。
第 3 级:标准化。 框架和架构是经过深思熟虑、为长期存续能力而选定的,这些选择被文档化,并在全组织范围内强制执行。渲染策略按界面进行匹配,渐进增强和优雅降级成为标准做法,共享组件库、代码检查和构建工具适用于每一个团队。跨浏览器兼容、无障碍和国际化被内置进来,而不是事后补上。
第 4 级:管理。 前端表现由数据来衡量和控制。性能预算在 CI 中被强制执行,性能倒退会导致构建失败,核心网页指标通过来自真实低端设备和慢速网络的真实用户监控进行追踪,并与明确的基准进行比对。打包体积、错误状态和离线状态的覆盖率,以及在最慢网络上获得服务的用户比例,都会被报告和评审,这样决策就建立在证据、而不是意见之上。
第 5 级:协同。 性能、弹性和覆盖面在整个组织范围内被持续改进,并与业务成果挂钩。前端依靠 Web 标准来获得耐久性,隔离框架依赖以降低迁移成本,并随着设备、平台和真实用户数据的变化而自适应地演进架构。全体公众和所有设备都被视为一等公民,前端实践与设计、无障碍和产品规划相集成,而不是被当作一个独立的关注点来对待。
讨论思路
- 你们如何判断一次框架迁移是否值得其成本和风险?
- 哪些核心网页指标和打包体积预算应当成为导致构建硬性失败的阈值?
- 渐进增强在哪些地方是必不可少的,客户端应用又在哪些地方是可以接受的?
- 你们如何在众多自治团队之间保持前端架构的一致性?
- 微前端在什么时候才真正值得其带来的复杂性?
- 应当如何把真实设备和慢速网络测试纳入到流水线之中?
关键要点
- 前端运行在一个你无法掌控的环境中:应针对多变性和失败来设计。
- 为长期存续的系统选择耐久、有良好支持的技术;依靠 Web 标准。
- 让渲染策略(SSR、SSG、CSR、流式渲染)匹配内容和需求,通常按路由混合使用。
- 把性能当作一项有预算、可衡量、并在 CI 中依据真实用户数据强制执行的纪律。
- 以渐进增强为原则来构建,让核心体验在任何地方都能正常工作。
- 发送更少的 JavaScript;进行代码拆分、懒加载,并优先使用平台自身能力。
- 对政府场景而言尤其如此:性能和弹性是实现公平访问的先决条件。
参考资料与延伸阅读
- Jeremy Keith, Resilient Web Design
- Aaron Gustafson, Adaptive Web Design (progressive enhancement)
- Steve Souders, High Performance Web Sites
- Ilya Grigorik, High Performance Browser Networking
- Addy Osmani, writings on performance, code-splitting, and the cost of JavaScript
- Google, Web Vitals and web.dev performance guidance
- MDN Web Docs, web platform and progressive enhancement references
- Alex Russell, essays on the cost of JavaScript and device diversity
- UK Government Digital Service, progressive enhancement and frontend guidance
- WHATWG HTML Living Standard and W3C web platform specifications