前端构建缓存和增量构建的原理是什么?如何提升大型项目构建速度?
简化版
构建缓存的核心是用输入指纹判断某个转换结果能否复用,增量构建的核心是只重新处理受影响模块。大型项目提速要从缓存命中率、依赖图范围、并行度、插件耗时、CI 缓存和任务编排几个方向一起做。
详细版
一次模块转换的输入不只是文件内容,还包括 loader/plugin 版本、配置、环境变量、依赖版本和目标浏览器。只要这些输入没变,就可以复用上次产物;如果变了,就要失效重算。
增量构建依赖模块图。改一个组件时,工具让该模块及受影响父链失效,而不是从入口全量重建。Monorepo 里还可以按包依赖图跳过未受影响包。
回答时要避免只说“开 cache”。真正有效的是明确缓存 key、缓存范围、失效条件和命中率,并用构建耗时分布找瓶颈。
完整版教学
一、构建慢通常慢在多段链路叠加
前端构建包含依赖安装、文件扫描、模块解析、语法转换、类型检查、CSS 处理、压缩、资源复制、Source Map 和产物上传。慢不一定在打包器本身。
install -> resolve -> transform -> typecheck -> bundle
-> minify -> sourcemap -> upload -> deploy
如果总耗时 12 分钟,其中安装 4 分钟、类型检查 3 分钟、压缩 2 分钟,盲目调 Webpack chunk 配置收益有限。工程优化第一步是拆耗时,而不是凭感觉改配置。
二、缓存的本质是输入相同则输出可复用
缓存不是把 dist 保存起来这么简单。每个任务或模块都需要一个输入指纹,指纹不变时才能复用输出。
记忆钩子:缓存命中靠“输入指纹稳定”,增量构建靠“影响范围准确”。
cache key = hash(
source content +
lockfile +
build config +
tool version +
env target +
browserslist
)
比如同一个 Button.tsx 文件,如果 TypeScript 版本、Babel 配置或 Browserslist 变了,转换结果都可能变化。好的缓存系统会把这些因素纳入 key,避免拿旧结果污染新构建。
三、增量构建依赖准确的依赖图
增量构建要回答两个问题:哪个输入变了,哪些输出受影响。模块依赖图越准确,重算范围越小;依赖图过粗,就会频繁退化为全量构建。
Button.tsx changed
-> Button.tsx transform invalid
-> ProductCard.tsx affected
-> HomePage chunk affected
-> AdminPage chunk unaffected
Monorepo 也是同理。如果 packages/ui/Button 变化,只需要重测依赖它的应用和包;如果根 tsconfig 或 lockfile 变化,影响范围就会扩大。
四、缓存命中率比“是否开启缓存”更重要
很多项目开启了缓存却依然慢,是因为 key 太敏感或缓存位置不稳定。比如每次 CI 都生成不同路径、环境变量包含时间戳、lockfile 经常被无关更新,都会降低命中率。
| 指标 | 含义 | 排查方向 |
|---|---|---|
| cache hit rate | 缓存命中比例 | key 是否过宽或过窄 |
| cold build | 无缓存耗时 | 基础构建能力 |
| warm build | 有缓存耗时 | 缓存收益 |
| affected tasks | 受影响任务数 | 依赖图是否准确 |
假设全量构建 10 分钟,理想 warm build 2 分钟。如果实际 warm build 仍是 8 分钟,说明缓存命中率、任务拆分或瓶颈定位存在问题。
五、CI 缓存要区分依赖缓存和构建缓存
依赖缓存常见对象是包管理器 store,比如 npm cache、pnpm store、yarn cache。构建缓存则是 Babel、Webpack、Vite、Turborepo、Nx 等任务输出或中间产物。
依赖缓存:下载过的包 -> 减少 install 网络和解压成本
构建缓存:任务输出/模块转换结果 -> 减少重复计算成本
两者都需要 lockfile 参与 key。lockfile 变了,依赖缓存可能仍能部分复用,但安装结果必须重新校验;构建缓存也可能因为依赖版本变化而失效。
六、提升大型项目速度的常见手段
先做低风险提速:限定 loader 范围、排除 node_modules 中不需要转换的包、开启持久化缓存、把类型检查和转译拆开、使用更快压缩器、减少 Source Map 成本。
| 手段 | 主要收益 | 注意点 |
|---|---|---|
| 持久化缓存 | 复用转换结果 | key 要准确 |
| 并行任务 | 利用多核 | 避免 IO 争抢 |
| affected 构建 | 跳过无关包 | 依赖图要准 |
| 类型检查拆分 | 缩短打包链路 | CI 仍需完整检查 |
| 远程缓存 | 复用团队构建结果 | 权限和一致性治理 |
如果本地保存一次平均 900 ms,降低到 180 ms 会明显改善开发体验;如果 CI 从 18 分钟降到 7 分钟,则能直接减少排队和发布等待。
七、缓存也会制造疑难问题
缓存错误最可怕的是“旧产物看起来能跑”。比如配置变了但 key 没变,旧转换结果被复用,可能导致本地和 CI 行为不同。
排查缓存问题时可以做 3 步:清缓存复现、打印 cache key 关键输入、比较冷构建与 warm 构建产物。不要把所有构建问题都归咎于缓存,但要知道缓存确实会隐藏问题。
团队实践上,缓存策略要可解释、可清理、可观测。比如提供 clean 脚本,CI 输出命中率,核心任务 key 变更要能从日志看出来。
八、常见误区与追问
- 误区:开启缓存后构建一定变快。 key 不稳定、命中率低或瓶颈在安装阶段时收益有限。
- 误区:缓存 dist 就等于构建缓存。 dist 是最终产物,构建缓存更关注任务输出和模块转换中间结果。
- 误区:增量构建只看改了哪些文件。 还要看依赖图、配置、lockfile、环境和工具版本。
- 追问:缓存 key 应该包含什么? 至少包含源码、lockfile、配置、工具版本、环境目标和影响输出的变量。
- 追问:为什么 CI 本地快但远端慢? 可能是缓存未恢复、依赖安装慢、机器性能差异或远程缓存未命中。
- 追问:如何评估优化效果? 比较 cold/warm 构建、cache hit rate、任务耗时分布和开发保存反馈时间。
九、加强记忆
构建提速按“拆耗时、算指纹、缩范围、看命中”记:先知道慢在哪,再用准确输入指纹复用结果,通过模块图和包依赖图做增量,最后用命中率和 cold/warm 对比验证。不要只回答“开缓存”,要讲清楚缓存为什么能复用、什么时候必须失效。