这是2025年的第34篇文章
( 本文阅读时间:15分钟 )
我于 2023 年 12 月负责前端构建持久化缓存服务的设计与开发工作,并先后于 2024 年 3 月,12 月上线编译缓存和依赖缓存服务,如今距离服务上线已 1 年有余,我觉得现在是一个很好的时机和各位读者分享我在复盘这个项目时的一些经验和反思,希望对大家有所帮助,也欢迎各位的反馈和建议。
01
背景
1.1 什么是前端构建持久化缓存
前端构建持久化缓存特指前端应用在构建过程中生成的可持久化的缓存文件,它主要包含安装依赖时产生的依赖缓存和使用特定构建工具(如 webpack@5 等)编译时产生的编译缓存,其核心目标是提升应用的构建效率。
这里有两点需要特别声明:
1.2 我们为什么要提供持久化缓存服务
比较显而易见的答案是「作为前端构建服务提供方,通过提供持久化缓存服务,我们能极大提升应用构建效率」。而如果要继续深究的话,我们有如下几点理由:
02
面临的挑战与应对之道
注意:依赖缓存和编译缓存核心都使用了前端持久化缓存的设计方案,但是鉴于编译缓存整体遇到的问题更多,并且核心由我负责设计,所以下文中大量示例和问题都来源于编译缓存。
2.1 应对系统复杂性
我是在刚来工程基础团队不满半年的条件下,临时接手的持久化缓存设计工作。对于当时的我而言,无论是即将接入的,负责提供缓存能力的 Taskline 集群,还是云构建以及围绕云构建的上下游系统都十分陌生。因此首当其冲要做的事就是全面梳理云构建系统架构,上下游关系以及摸清依赖方能力边界。
云构建作为一个在线上迭代多年的前端构建老牌服务,其内部的依赖关系,与二方系统的交互流程较为复杂,要想妥善的将持久化缓存插入其中,需要找准一个合适的角度,将对系统整体复杂度的影响降到最低。
为此,我在前期花了大量时间梳理云构建的相关信息,事后回看,这一阶段的投入非常必要。
整个 TRE 团队强调「设计左移」,我认为这非常正确,恰如一句老话「磨刀不误砍柴工」,我在此也想要向您特别强调这一经验:「在做任何设计前,务必尽力保障您已经对整个系统的架构,系统的上下游关系有较为深入的了解」。
就我的观察来看,大多数有问题的系统设计,要么前期对系统及其上下游关系的了解不够深入,要么是仅深入了解了系统架构,忽略了系统与上下游之间复杂的依赖关系。因此我认为雷军有句话放在这里非常合适「不要用战术上的勤奋去掩盖战略上的懒惰」。
除此之外,我还想和您分享我的两点心得:
2.2 明确稳定
在前端构建这个场景,我始终认为稳定性是第一位,任何新功能可以上线的前提条件是不会影响现有的构建流程。为此,在保障新功能上线后,构建系统的稳定性,我们做了如下工作:
JavaScript 与 Java,Go,Rust 等一众语言的一大不同之处在于,JavaScript 没有统一的工程套件,前端开发者在编译工具,bundler 的选择上非常灵活,这就使持久化缓存功能的提供者非常多元,缓存质量也参差不齐,由此带来的挑战是:
为此,我们深扒源码文档,详细调研了前端主流编译工具的缓存机制,做出了如下设计:
这看似两点小的限制,却是我们深度调研和实践结果的总结:前端构建领域,编译工具生成的持久化缓存并非 100% 可靠的,通过开启锁依赖,可以极大提升持久化缓存的可靠性。
作为前端构建平台,我们很难为众多应用去调优缓存生成,判断逻辑,但是我们可以做到的是及时止损,在功能出现系统性问题或单点问题时,都能立即中断,即时止损,不影响业务构建的稳定。
为此,我们建设了三级熔断机制,保障各个层面缓存服务的可靠性:
构建器是云构建的一个领域概念,本质上是对相同构建逻辑的封装,多个应用可通过复用一个构建器执行相同的构建操作。
基于以上设计,我们能确保服务上线后基本不出问题,出了问题也能立即解决或止损,对于此类辅助功能的上线,提前给自己留好退路,在出现紧急问题时能通过快速熔断获得一段 debug 的时间缓冲,并通过对原理机制的了解快速定位问题,这种弹性设计能使我始终从容的面对新功能的发布,是我始终坚持的设计原则。
2.3 极致复用
我们始终追求打造「卓越工程」,在系统设计上,我们追求「精简」和「高效」的设计,对于前端持久化缓存服务而言:
这两点是构建持久化缓存服务的核心,下面将重点介绍其设计理念和经验。
无论是缓存的安装(首次安装约 8 秒)还是缓存有效性的判断都会增加额外的构建时长,理想情况下,当一份缓存不可用时,应该直接跳过缓存的安装,直接进入构建流程,从而避免不必要的性能损耗。
为了实现这个效果,我们平衡了主流构建工具缓存判断原理,前端开发经验和云构建系统现有状况,在构建节点中添加了缓存预检机制,通过快速计算核心影响缓存有效性的文件(默认包含了 package-lock.json
和一些系统环境变量,并允许用户通过配置声明添加新文件)哈希,判断是否跳过缓存安装流程。
通过缓存预检机制,我们能够快速判断一份缓存是否有效,让注定无效的缓存不要进入缓存复用流程。接下来我们需要保障一份缓存尽可能的被复用,换句话说,提升有效缓存的利用率。
为此,我们综合考虑了实际的开发场景,设计了迭代,应用两级维度的缓存:迭代间的缓存可复用,迭代发布后,迭代缓存将自动升级为应用级别的缓存,从而实现每个应用仅首次未发布迭代无缓存的效果。
之所以缓存以迭代为最小单位,是因为一般而言,一个迭代中功能的相关性较强,不容易产生如依赖变动等导致缓存全局失效的变动,如果以应用维度隔离缓存,则会导致缓存频繁失效,更新,若以变更维度隔离缓存,则导致每个变更在初次构建时均无缓存可用,使缓存效用降低。
因此迭代粒度的缓存是权衡了缓存复用性和稳定性后的选择,而发布后迭代缓存自动升级为应用缓存,则符合分支合并后,缓存为应用最新可靠缓存的研发逻辑。
2.4 用户拓展
俗话说「行百里者半九十」,对于持久化缓存服务而言,实现功能只是完成了工作的一半。工作的另一半,也是编译缓存一直以来需要面对的挑战是 —— 如何拓展更多用户接入。
功能推广对于大多数工程师而言都是一个棘手的问题,我们也在这个问题上绞尽脑汁,下面将向您分享我们对于这一挑战的思考与实践。
我们花了相当的时间搞清楚了一件事:用户不是越多越好,因为对于编译缓存而言,当应用的构建时长并未成为性能瓶颈(构建耗时 < 60 秒)时,接入编译缓存的好处并不明显,在一些情况下反而会造成性能劣化。因此我们需要抑制疯狂提升用户数量的冲动,站在用户利益的角度思考另一个问题:谁适合接入此服务,在这些应用中,谁尚未接入?
在持久化缓存服务上线后,我们时常面临的一类用户问题是:“我的缓存是否命中?命中了多少?”。从功能的完整性上,这是一个好问题,但是从开发成本上,特别是编译缓存的开发成本上看,要回答清楚这个问题,却一点也不轻松。一方面,前端编译工具种类丰富,不同工具的缓存统计方式不同,另一方面,即使是一种编译工具,要想彻底搞清其缓存机制,并向用户透出缓存命中信息也需要花大量时间投入调研。
最终,我们大概投入了近 1 个月时间搞清楚了前端主流构建工具 webpack@5 的缓存机制和命中率统计方式,并提供了插件帮助用户搞清当前缓存命中情况,下图是运行后的显示效果:
通过这一阶段的调研和开发,一方面对于用户而言,缓存的使用更加透明,服务更加值得信赖,另一方面,我们对 webpack 的缓存机制有更透彻的了解,让我们在提供其他服务时更有信心。
除了设计语义化的配置项和 API,提供友好的用户文档之外,最理想的降低使用成本的方式是让用户使用功能的成本降为 0,虽然这乍看之下似乎很难做到,不过我们还是找到了一些局部的解决之道:
「构建器」是云构建特有的概念,本质上是通过 npm 包封装了一段构建逻辑以便于同一类型应用复用,因此在持久化缓存设计之初,我们就支持了构建器维度的批量缓存开启配置。
在功能推广遇到瓶颈之后,我们通过内部调研问卷获取用户未接入该功能的原因。从用户反馈来看,有近五成的被调用户反馈接入编译缓存的成本依然较高。
这驱使我们进一步探索如何降低功能接入成本,为此,我们对近 7 天的构建任务进行回放实验,探查存在持久化缓存文件的应用以及缓存可复用情况,根据回放实验的数据显示,有近一半任务存在隐式缓存(即编译时生成持久化缓存文件,但应用自身未开启持久化缓存功能),当复用隐式缓存后,平均减少约 27.61% 构建耗时,并且编译时间越久的应用,复用缓存的收益越明显:
基于此数据,我们设计了一套为存在隐式缓存项目自动开启编译缓存的方案,当在日常发布环境发现应用存在隐式缓存时,默认为用户开启持久化缓存功能,并提供便捷的操作入口供用户及时关闭。通过这一举措,我们成功将活跃任务的编译缓存覆盖率从 11% 提升至 62.05%。
在确保开发的功能的确能切实解决实际问题后,接下来我们就要通过各种宣传手段,尽可能让更多用户知道这个功能有用,并有明确的渠道知道该这个功能该如何使用,以下是我们尝试的宣传手段:
虽然从数据上看,通过各种宣传所带来的新用户数量并不亮眼,但是它确实帮助我们吸纳了一部分天使用户,收获了一些新需求和建议,并且在未来相当长的时间里,帮助我们摆脱了围绕新功能的种种答疑工作。因此,综合来看,各种渠道的宣传,尤其是撰写用户手册并持续维护是非常重要的。
03
成果与不足
3.1 成果
至此,我向您介绍了在设计,落地前端构建持久化缓存服务中的思考和实践,下面是该服务上线后取得的性能收益(数据截止 2025.3.16):
依赖缓存覆盖率 49.06%(针对开启锁依赖任务全量开启),开启依赖缓存后,依赖安装性能总体提升约 23.91%(减少约 3.33 秒),构建耗时越大的项目,性能收益越明显,其中:
3.2 不足
04
经验总结
感谢您的耐心阅读,我在这里准备了一份脱水版的实践经验总结,便于您回顾,希望对您的工作思考有所助益,也期待您的交流和反馈:
4.1 通用经验
4.2. 前端经验
参考链接