cover_image

前端构建持久化缓存服务的设计与经验总结

空堂 阿里技术
2025年04月16日 00:32
图片



这是2025年的第34篇文章

( 本文阅读时间:15分钟 )



我于 2023 年 12 月负责前端构建持久化缓存服务的设计与开发工作,并先后于 2024 年 3 月,12 月上线编译缓存和依赖缓存服务,如今距离服务上线已 1 年有余,我觉得现在是一个很好的时机和各位读者分享我在复盘这个项目时的一些经验和反思,希望对大家有所帮助,也欢迎各位的反馈和建议。



01



背景

1.1 什么是前端构建持久化缓存

前端构建持久化缓存特指前端应用构建过程中生成可持久化缓存文件,它主要包含安装依赖时产生的依赖缓存和使用特定构建工具(如 webpack@5 等)编译时产生的编译缓存,其核心目标是提升应用的构建效率


这里有两点需要特别声明:

  • 在前端领域,并非所有构建工具都支持生成持久化缓存(例如 webpack < 4),也并非所有构建工具生成的持久化缓存都是可靠的(例如 webpack@4);
  • 作为前端工程团队,我们本质上是要借缓存解决云端构建效率问题,因此对于我们而言,只有持久化缓存才适用于我们的需求场景;

1.2 我们为什么要提供持久化缓存服务

比较显而易见的答案是「作为前端构建服务提供方,通过提供持久化缓存服务,我们能极大提升应用构建效率」。而如果要继续深究的话,我们有如下几点理由:


外部拉力

1.用户有真实需求:从我们的大盘数据来看,有较多任务构建耗时 > 200 秒,对于大型的多包应用,构建耗时在十几分钟甚至几十分钟,这对于开发者而言经常是难以忍受的;

2.数据上看有明确收益:
  • 前期探索的数据来看,部分应用接入持久化缓存后,可提升约 114.5% 的构建效率
  • 从前端构建大盘数据来看,有近一半应用使用了支持持久化缓存的编译工具(webpack 4,5),开启编译缓存收益巨大;


内部推力

1.卓越工程需要:自 2019 年 10 月 webpack 5 正式宣布支持持久化缓存,距今已经 4 年了,随后越来越多的编译工具也将支持持久化缓存作为默认的特性,作为支撑 1+6+n 前端工程的团队,提供持久化缓存服务理应是职责所在;

2.借助 Taskline 集群底层能力,精简系统架构:部分用户可能有印象,我们之前已经有建设一次持久化缓存能力,但是由于种种原因,效果并不理想。而这一次我们有机会和持续集成平台团队合作,基于 Taskline 集群提供的缓存能力,搭建持久化缓存服务,从短期来看,这有利于我们精简系统架构,把更多精力放在业务逻辑上,通过合理分工让收益最大化,从长期来看,通过缓存能力,让构建任务逐渐过渡至新集群,也有利于未来让用户享受到更稳定,高效的基础服务。


02



面临的挑战与应对之道

注意:依赖缓存和编译缓存核心都使用了前端持久化缓存的设计方案,但是鉴于编译缓存整体遇到的问题更多,并且核心由我负责设计,所以下文中大量示例和问题都来源于编译缓存。

2.1 应对系统复杂性

我是在刚来工程基础团队不满半年的条件下,临时接手的持久化缓存设计工作。对于当时的我而言,无论是即将接入的,负责提供缓存能力的 Taskline 集群,还是云构建以及围绕云构建的上下游系统都十分陌生。因此首当其冲要做的事就是全面梳理云构建系统架构,上下游关系以及摸清依赖方能力边界。


云构建作为一个在线上迭代多年的前端构建老牌服务,其内部的依赖关系,与二方系统的交互流程较为复杂,要想妥善的将持久化缓存插入其中,需要找准一个合适的角度,将对系统整体复杂度的影响降到最低


为此,我在前期花了大量时间梳理云构建的相关信息,事后回看,这一阶段的投入非常必要。


整个 TRE 团队强调「设计左移」,我认为这非常正确,恰如一句老话「磨刀不误砍柴工」,我在此也想要向您特别强调这一经验:「在做任何设计前,务必尽力保障您已经对整个系统的架构,系统的上下游关系有较为深入的了解」。


就我的观察来看,大多数有问题的系统设计,要么前期对系统及其上下游关系的了解不够深入,要么是仅深入了解了系统架构,忽略了系统与上下游之间复杂的依赖关系。因此我认为雷军有句话放在这里非常合适「不要用战术上的勤奋去掩盖战略上的懒惰」。


除此之外,我还想和您分享我的两点心得:


1.不要钻牛角尖,无需事无巨细:虽然我一再强调前期调研工作的重要性,但是在调研阶段投入太多和太少的时间性价比都很有限,我有时会花很多时间在前期调研上,有点强迫症的想要避免任何问题发生从而证明我是一位出色的工程师,但是遗憾的是「问题总会发生」,并且事后复盘来看,在前期调研上投入的多余时间几乎是没有价值的。那么什么时候应该在调研阶段「及时收手」呢?我的建议是:

  • 在调研期间,经常问自己当下调研的内容和自己即将做的设计,关联性是什么,避免在调研途中迷失
  • 当设计中面临的所有的问题都能被自己回答后,停止调研,进入技术评审环节,耐心等待他人的反馈

2.多沟通,多实践:在前期调研阶段,积极向相关人员请教自己困惑的问题能够极大提升调研效率,但是值得注意的事,文档可能腐化,他人的印象也未必可靠,每个设计的核心环节都需要实践证明其有效性。我在这次的设计的过程中就因为没有验证他人的经验而做出了冗余的设计,最后导致下游的改造成本很高,虽然我们最后及时解决了这个问题,但是这个问题理应在设计阶段被规避掉;

2.2 明确稳定

在前端构建这个场景,我始终认为稳定性是第一位,任何新功能可以上线的前提条件是不会影响现有的构建流程。为此,在保障新功能上线后,构建系统的稳定性,我们做了如下工作:


搞懂缓存原理,限制服务范围

JavaScript 与 Java,Go,Rust 等一众语言的一大不同之处在于,JavaScript 没有统一的工程套件,前端开发者在编译工具,bundler 的选择上非常灵活,这就使持久化缓存功能的提供者非常多元,缓存质量也参差不齐,由此带来的挑战是:


  1. 我们需要尽力掌握不同编译工具的持久化缓存生成,有效性判断机制,从原理上判定该份缓存是否可靠,以及如何通过配置将一份不可靠的缓存变为可靠
  2. 我们需要在系统层面上限制持久化缓存服务的生效场景,从而避免无法预防的不可靠缓存对业务产生不利影响


为此,我们深扒源码文档,详细调研了前端主流编译工具的缓存机制,做出了如下设计:


  1. 对于非用户主动开启的持久化缓存,仅针对有锁依赖文件的应用生效;
  2. 鉴于部分编译工具会持久化缓存编译产物,仅在日常环境开启持久化缓存服务;


这看似两点小的限制,却是我们深度调研和实践结果的总结:前端构建领域,编译工具生成的持久化缓存并非 100% 可靠的,通过开启锁依赖,可以极大提升持久化缓存的可靠性。


三级中断机制,及时止损

作为前端构建平台,我们很难为众多应用去调优缓存生成,判断逻辑,但是我们可以做到的是及时止损,在功能出现系统性问题或单点问题时,都能立即中断,即时止损,不影响业务构建的稳定。


为此,我们建设了三级熔断机制,保障各个层面缓存服务的可靠性:


1.应用粒度:用户可通过配置文件主动声明服务的开启或关闭;

2.对于默认开启的持久化缓存服务的应用(见下文),可通过构建面板感知,通过配置项关闭;

图片

3.构建器粒度:为构建器 owner 提供一键开关编译缓存服务的配置;

构建器是云构建的一个领域概念,本质上是对相同构建逻辑的封装,多个应用可通过复用一个构建器执行相同的构建操作。


4.平台粒度:支持 BU 维度或全局维度的服务关闭。

基于以上设计,我们能确保服务上线后基本不出问题,出了问题也能立即解决或止损,对于此类辅助功能的上线,提前给自己留好退路,在出现紧急问题时能通过快速熔断获得一段 debug 的时间缓冲,并通过对原理机制的了解快速定位问题,这种弹性设计能使我始终从容的面对新功能的发布,是我始终坚持的设计原则

2.3 极致复用

我们始终追求打造「卓越工程」,在系统设计上,我们追求「精简」和「高效」的设计,对于前端持久化缓存服务而言:


  • 「精简」体现在我们通过「缓存预检机制」对缓存可用性进行预判,只有在缓存可用时才加载缓存,从而避免了无用缓存加载带来的性能损耗
  • 「高效」体现在我们设计了「迭代维度的双级缓存机制」,让缓存尽可能的被有效复用;


这两点是构建持久化缓存服务的核心,下面将重点介绍其设计理念和经验。


缓存预检机制

无论是缓存的安装(首次安装约 8 秒)还是缓存有效性的判断都会增加额外的构建时长,理想情况下,当一份缓存不可用时,应该直接跳过缓存的安装,直接进入构建流程,从而避免不必要的性能损耗。


为了实现这个效果,我们平衡了主流构建工具缓存判断原理,前端开发经验和云构建系统现有状况,在构建节点中添加了缓存预检机制,通过快速计算核心影响缓存有效性的文件(默认包含了 package-lock.json 和一些系统环境变量,并允许用户通过配置声明添加新文件)哈希,判断是否跳过缓存安装流程


迭代粒度的双级缓存机制

通过缓存预检机制,我们能够快速判断一份缓存是否有效,让注定无效的缓存不要进入缓存复用流程。接下来我们需要保障一份缓存尽可能的被复用,换句话说,提升有效缓存的利用率


为此,我们综合考虑了实际的开发场景,设计了迭代,应用两级维度的缓存:迭代间的缓存可复用,迭代发布后,迭代缓存将自动升级为应用级别的缓存,从而实现每个应用仅首次未发布迭代无缓存的效果


之所以缓存以迭代为最小单位,是因为一般而言,一个迭代中功能的相关性较强,不容易产生如依赖变动等导致缓存全局失效的变动,如果以应用维度隔离缓存,则会导致缓存频繁失效,更新,若以变更维度隔离缓存,则导致每个变更在初次构建时均无缓存可用,使缓存效用降低。


因此迭代粒度的缓存是权衡了缓存复用性和稳定性后的选择,而发布后迭代缓存自动升级为应用缓存,则符合分支合并后,缓存为应用最新可靠缓存的研发逻辑。

2.4 用户拓展

俗话说「行百里者半九十」,对于持久化缓存服务而言,实现功能只是完成了工作的一半。工作的另一半,也是编译缓存一直以来需要面对的挑战是 —— 如何拓展更多用户接入。


功能推广对于大多数工程师而言都是一个棘手的问题,我们也在这个问题上绞尽脑汁,下面将向您分享我们对于这一挑战的思考与实践。


确定用户群体

我们花了相当的时间搞清楚了一件事:用户不是越多越好,因为对于编译缓存而言,当应用的构建时长并未成为性能瓶颈(构建耗时 < 60 秒)时,接入编译缓存的好处并不明显,在一些情况下反而会造成性能劣化。因此我们需要抑制疯狂提升用户数量的冲动,站在用户利益的角度思考另一个问题:谁适合接入此服务,在这些应用中,谁尚未接入?


持续满足用户的合理诉求

在持久化缓存服务上线后,我们时常面临的一类用户问题是:“我的缓存是否命中?命中了多少?”。从功能的完整性上,这是一个好问题,但是从开发成本上,特别是编译缓存的开发成本上看,要回答清楚这个问题,却一点也不轻松。一方面,前端编译工具种类丰富,不同工具的缓存统计方式不同,另一方面,即使是一种编译工具,要想彻底搞清其缓存机制,并向用户透出缓存命中信息也需要花大量时间投入调研。


最终,我们大概投入了近 1 个月时间搞清楚了前端主流构建工具 webpack@5 的缓存机制和命中率统计方式,并提供了插件帮助用户搞清当前缓存命中情况,下图是运行后的显示效果:


图片


通过这一阶段的调研和开发,一方面对于用户而言,缓存的使用更加透明,服务更加值得信赖,另一方面,我们对 webpack 的缓存机制有更透彻的了解,让我们在提供其他服务时更有信心。


降低功能使用成本

除了设计语义化的配置项和 API,提供友好的用户文档之外,最理想的降低使用成本的方式是让用户使用功能的成本降为 0,虽然这乍看之下似乎很难做到,不过我们还是找到了一些局部的解决之道:


构建器维度开启持久化缓存

「构建器」是云构建特有的概念,本质上是通过 npm 包封装了一段构建逻辑以便于同一类型应用复用,因此在持久化缓存设计之初,我们就支持了构建器维度的批量缓存开启配置。


默认为含有隐式缓存应用开启持久化缓存

在功能推广遇到瓶颈之后,我们通过内部调研问卷获取用户未接入该功能的原因。从用户反馈来看,有近五成的被调用户反馈接入编译缓存的成本依然较高。


这驱使我们进一步探索如何降低功能接入成本,为此,我们对近 7 天的构建任务进行回放实验,探查存在持久化缓存文件的应用以及缓存可复用情况,根据回放实验的数据显示,有近一半任务存在隐式缓存(即编译时生成持久化缓存文件,但应用自身未开启持久化缓存功能),当复用隐式缓存后,平均减少约 27.61% 构建耗时,并且编译时间越久的应用,复用缓存的收益越明显


基于此数据,我们设计了一套为存在隐式缓存项目自动开启编译缓存的方案,当在日常发布环境发现应用存在隐式缓存时,默认为用户开启持久化缓存功能,并提供便捷的操作入口供用户及时关闭。通过这一举措,我们成功将活跃任务的编译缓存覆盖率从 11% 提升至 62.05%。


宣传,宣传,持续宣传

在确保开发的功能的确能切实解决实际问题后,接下来我们就要通过各种宣传手段,尽可能让更多用户知道这个功能有用,并有明确的渠道知道该这个功能该如何使用,以下是我们尝试的宣传手段:


1.引导老版编译缓存的用户升级至新方案;
2.撰写详尽的功能文档 —《如何接入云端编译缓存》指导用户如何 step by step 接入;
3.在用户群,O2 Space 平台公告等能向用户传递消息的一切场合透出功能消息;
4.和一线答疑同学联动,当发现目标场景时,主动推荐用户接入功能。


虽然从数据上看,通过各种宣传所带来的新用户数量并不亮眼,但是它确实帮助我们吸纳了一部分天使用户,收获了一些新需求和建议,并且在未来相当长的时间里,帮助我们摆脱了围绕新功能的种种答疑工作。因此,综合来看,各种渠道的宣传,尤其是撰写用户手册并持续维护是非常重要的。



03



成果与不足

3.1 成果

至此,我向您介绍了在设计,落地前端构建持久化缓存服务中的思考和实践,下面是该服务上线后取得的性能收益(数据截止 2025.3.16):


  • 依赖缓存覆盖率 49.06%(针对开启锁依赖任务全量开启),开启依赖缓存后,依赖安装性能总体提升约 23.91%(减少约 3.33 秒),构建耗时越大的项目,性能收益越明显,其中:

    • 构建耗时 <100s 应用,依赖安装性能提升 10.81%;
    • 构建耗时 100~200s 之间应用,依赖安装性能提升 31.68%;
    • 构建耗时 200~300s 之间应用,依赖安装性能提升 32.21%;
    • 构建耗时 300~400s 之间应用,依赖安装性能提升 30.02%;
    • 构建耗时 >400s 应用,依赖安装性能提升 43.49%;

  • 编译缓存覆盖率 62.05%,开启编译缓存后,编译性能总体提升约 18.37%(减少约 15.72 秒),构建耗时越大的项目,性能收益越明显,其中:
    • 构建耗时 <100s 应用,编译性能提升 7.62%;
    • 构建耗时 100~200s 之间应用,依赖安装性能提升 16.45%;
    • 构建耗时 200~300s 之间应用,依赖安装性能提升 20.09%;
    • 构建耗时 300~400s 之间应用,依赖安装性能提升 28.53%;
    • 构建耗时 >400s 应用,依赖安装性能提升 31.64%;

3.2 不足

1.直至今日,我们依然无法 100% 保障所有编译缓存可以被安全复用,这一方面制约了我们将持久化缓存能力推广至线上发布环境,另一方面也在少数场景下,为用户排查构建问题带来了额外的成本,这个问题本质上受困于前端丰富的工程选型与工程团队有限的人力投入,很遗憾,在短期内只能保持现状;

2.对于一些采用落后技术选型的老应用(包括未开启锁依赖应用),我们尚未有有效手段推动其升级,这使得这些应用无法受惠于已建设的持久化缓存服务,这将是我们下一步发力的方向。


04



经验总结

感谢您的耐心阅读,我在这里准备了一份脱水版的实践经验总结,便于您回顾,希望对您的工作思考有所助益,也期待您的交流和反馈:

4.1 通用经验

  • 在行动之前,先考虑动机是否合理,是否有 MVP 的数据验证,选择错误的方向,走的越多,离目标越远;
  • 当对设计的对象不熟悉时,务必保障了解系统和系统上下游的关系,边设计边自我质疑,直到所有的问题都被回答;
  • 遇到不了解的事情,积极与知情人沟通,但是别忘了实践验证;
  • 在设计新功能时,有条件的情况下设计多级中断机制,保障紧急情况下优雅降级;
  • 好的系统设计是「简洁」「高效」的系统设计,设计时检查系统整条链路存在的冗余环节,想办法优化掉它;
  • 站在用户角度思考,不要一味推广;
  • 功能推广有多种渠道,测试,答疑,和你功能有关的一切同学都是可以帮助你推广功能的对象;
  • 系统上保留验证通道,一条让你能安心验证假设的实验路径,这样在遇到问题时,你可以有地方「大胆假设,小心求证」,有可能获得超额收益。

4.2. 前端经验

  • 不同编译工具的缓存可靠性不同,不建议平台侧统一控制缓存的开闭;
  • 锁依赖是保障依赖缓存,编译缓存稳定性的核心要件;
  • 编译缓存的生成,判定会耗费额外的构建时间,并非所有类型的应用都适合使用。

参考链接

[01] webpack 5 正式宣布支持持久化缓存
https://github.com/webpack/changelog-v5/blob/master/guides/persistent-caching.md

图片



图片
欢迎留言一起参与讨论~

继续滑动看下一个
阿里技术
向上滑动看下一个