基于本文回答

播面 播面

文解图解播客视频,多维讲透八股文
0
评论

Webpack 5 的持久化缓存(Persistent Caching)原理是什么?

Webpack 5 的持久化缓存(Persistent Caching)是其最具革命性的特性之一。它允许 Webpack 将构建过程中的编译结果和模块图结构(Module Graph)序列化后存储到磁盘(File System)上,在下一次构建时直接复用,从而大幅度提升二次构建(尤其是冷启动)的速度。

在 Webpack 4 及更早版本中,我们需要依赖第三方插件(如 hard-source-webpack-plugin)或特定的 loader 缓存(如 cache-loaderbabel-loader?cacheDirectory)来实现类似功能。而 Webpack 5 将其提升为了原生内置核心能力

以下是 Webpack 5 持久化缓存的核心工作原理:


1. 核心缓存对象:缓存的不仅是代码,而是“图(Graph)”

传统 loader 缓存只保存“单文件转换后的代码”,而 Webpack 在构建时最耗时的部分之一是依赖图的构建(Module Graph)和 Chunk 图的生成(Chunk Graph)

Webpack 5 的持久化缓存会序列化保存以下内容:

  • 模块对象(Module):包括解析后的 AST、依赖关系、转译后的代码。
  • 依赖图(Module Graph)和分块图(Chunk Graph):整体项目的拓扑结构。
  • 资源信息(Assets):最终输出的文件信息。

原理:再次构建时,如果缓存有效,Webpack 可以跳过递归解析 AST 和分析依赖的过程,直接将磁盘上的图结构反序列化回内存,瞬间完成依赖图的恢复。


2. 快照机制(Snapshot Algorithm):高效检测变更

为了判断磁盘上的缓存是否仍然有效,Webpack 5 引入了文件系统快照(Snapshot)机制,而不是简单地对所有文件做 Hash(那太慢了)。

快照包含两种判定模式:

  1. 时间戳(Timestamp / mtime:比较文件的最后修改时间。
  2. 文件内容 Hash:如果时间戳变了,再进一步对比文件内容的 Hash 值(避免仅仅 touch 了一下文件就导致缓存失效)。

优化的快照策略(Managed Paths)

项目依赖的 node_modules 文件极其庞大,如果对每个文件都做快照比对,性能会非常差。Webpack 5 对此做了分级处理:

  • managedPaths(托管路径):默认包含 node_modules。Webpack 认为这些包只有在安装/更新时才会变,因此只检测 package.json 的版本号或外层目录的时间戳,跳过内部几万个文件的逐个比对。
  • immutablePaths(不可变路径):比如包含 Hash 的路径或绝对不会改变的路径,直接跳过所有检查。
  • 源码路径:进行细化校验(时间戳 + Hash)。

3. 高效的序列化与反序列化(Serialization)

为了将内存中的 JavaScript 对象(特别是庞大且复杂的 ModuleGraph)快速写入磁盘并读取,Webpack 5 并没有使用原生的 JSON.stringify/parse(性能差且无法处理循环引用、函数、Buffer 等)。

Webpack 5 实现了一套自定义的高性能二进制序列化器(Serializer)

  • 支持处理复杂的数据结构(如循环依赖、MapSetBuffer 等)。
  • 采用了类似 Protocol Buffers 的紧凑二进制格式,写入和读取速度极快,磁盘占用小。

4. 缓存失效(Cache Invalidation)的触发条件

Webpack 5 在生成缓存时,会计算一个缓存键(Cache Key),同时记录影响构建的全局依赖。以下任何一项发生变化,缓存都会自动失效并重新构建:

  1. 源码变更:快照检测到某些文件的时间戳/Hash 不一致。
  2. 配置变更webpack.config.js 被修改。Webpack 默认会将配置文件及其依赖加入到 buildDependencies 中。
  3. 依赖包变更node_modules 中安装了新包或更新了版本(通过检查 package.json / lockfile)。
  4. 环境/CLI 变更:Node.js 版本改变、Webpack 版本改变、传递给 CLI 的环境变量改变。

5. 持久化缓存的工作流程图解

plaintext
[构建开始]
   │
   ▼
1. 计算 Cache Key (包含 Webpack 版本、配置、环境变量等)
   │
   ├─► [无缓存/Key 不匹配] ──► 全量重新构建 ──► 写入新缓存到磁盘 ──► [结束]
   │
   ▼ [命中有效缓存目录]
2. 检查 Snapshot (对比文件时间戳/Hash/node_modules)
   │
   ├─► [快照失效] ──────────► 增量构建 (仅编译变动模块) ──► 更新磁盘缓存 ──► [结束]
   │
   ▼ [快照全部有效 (完全命中)]
3. 读取磁盘二进制缓存,反序列化恢复 ModuleGraph / ChunkGraph
   │
   ▼
4. 跳过 Parse / Transform / Seal 阶段,直接生成 Output
   │
   ▼
[构建完成] (速度提升数倍至数十倍)

6. 配置示例

在 Webpack 5 中开启持久化缓存非常简单:

javascript
// webpack.config.js
module.exports = {
  // ...
  cache: {
    type: 'filesystem', // 使用文件系统持久化缓存
    buildDependencies: {
      // 当配置文件发生变化时,使缓存失效
      config: [__filename],
    },
    name: 'development-cache', // 缓存名称,不同环境建议分开
  },
};

缓存文件默认存储在 node_modules/.cache/webpack 目录下。

总结

Webpack 5 持久化缓存的核心原理可以概括为:
“基于自定义序列化技术将 ModuleGraph 等内存图结构存盘,结合基于分级的 Snapshot 快照算法精准检测变更,实现冷启动时的极致秒开。” 这一机制彻底解决了大中型前端项目构建缓慢的痛点。