模块构建缓存
1. 使用
模块构建缓存,推荐使用 Webpack 5 的 filesystem cache
,相比 umi 的 mfsu 技术更成熟。开启模块构建缓存,可以显著提升构建速度:
module.exports = {
cache: {
// 将缓存类型设置为文件系统,默认 memory
type: 'filesystem',
version: createEnvironmentHash(env.raw),
cacheDirectory: paths.appWebpackCache,
store: 'pack',
// 指定何时让缓存失效
buildDependencies: {
defaultWebpack: ['webpack/lib/'],
// 更改配置文件时,重新缓存
config: [__filename],
tsconfig: [paths.appTsConfig, paths.appJsConfig].filter(f =>
fs.existsSync(f)
),
},
},
}
关于持久化缓存,有两个地方需要注意:
- 默认缓存的路径是
node_modules/.cache/webpack
,也就是说,只要删除node_modules
,相当于缓存也被清空了 - 本地和 CI 环境的缓存是相互独立的,本地的缓存无法在 CI 环境使用。在 CI 环境中需要使用 CI 的缓存机制
2. 原理
为什么开启持久化缓存之后构建性能会有如此巨大的提升呢?一言蔽之,Webpack5 会将首次构建出的 Module、Chunk、ModuleGraph 等对象序列化后保存到硬盘中,后面再运行的时候就可以跳过一些耗时的编译动作,直接复用缓存信息。
1) 构建流程
Webpack 的构建过程大致上可划分为三个阶段:
- 初始化,主要是根据配置信息设置内置的各类插件
- Make - 构建阶段,从 entry 模块开始,执行:
- 读入文件内容
- 调用 Loader 转译文件内容
- 调用 acorn 生成 AST 结构
- 分析 AST,确定模块依赖列表
- 遍历模块依赖列表,对每一个依赖模块重新执行上述流程,直到生成完整的模块依赖图 —— ModuleGraph 对象
- Seal - 生成阶段,过程:
- 代码转译,如 import 转换为 require 调用
- 分析运行时依赖
- 遍历模块依赖图,对每一个模块执行:
- 合并模块代码与运行时代码,生成 chunk
- 执行产物优化操作,如 Tree-shaking
- 将最终结果写出到产物文件
过程中存在许多 CPU 密集型操作,例如调用 Loader 链加载文件时,遇到 babel-loader、eslint-loader、ts-loader 等工具时可能需要重复生成 AST;分析模块依赖信息时则需要遍历 AST,执行大量运算;Seal 阶段也同样存在大量 AST 遍历,以及代码转换、优化操作,等等。
2) 实现缓存
在引入持久化缓存之前,Webpack 在每次运行时都需要对所有模块完整执行上述构建流程,假设业务项目中有 1000 个文件,则每次执行 npx webpack
命令时都需要从 0 开始执行 1000 次构建、生成逻辑。
而 Webpack5 的持久化缓存功能则尝试将构建结果保存到文件系统中,在下次编译时对比每一个文件的内容哈希或时间戳,未发生变化的文件跳过编译操作,直接使用缓存副本,减少重复计算;发生变更的模块则重新执行编译流程。缓存执行时机如下图:
如图,Webpack 在首次构建完毕后将 Module、Chunk、ModuleGraph 三类对象的状态序列化并记录到缓存文件中;在下次构建开始时,尝试读入并恢复这些对象的状态,从而跳过执行 Loader 链、解析 AST、解析依赖等耗时操作,提升编译性能。
3. Webpack 4 中的缓存
实际上,Webpack 4 已经内置使用内存实现的临时缓存功能,但必须在 watch
模式下使用,进程退出后立即失效,实用性不高。不过,在 Webpack 4 及之前版本中可以使用一些 loader
自带的缓存功能提升构建性能,例如 babel-loader
、eslint-loader
、cache-loader
。
1) 开启 babel-loader 缓存
只需设置 cacheDirectory = true
即可开启 babel-loader
持久化缓存功能,例如:
module.exports = {
// ...
module: {
rules: [{
test: /\.m?js$/,
loader: 'babel-loader',
options: {
cacheDirectory: true,
},
}]
},
// ...
};
以 Three.js 为例,开启缓存后生产环境构建耗时从 3500ms 降低到 1600ms;开发环境构建从 6400ms 降低到 4500ms,性能提升约 30% ~ 50% 。
默认情况下,babel-loader
会将缓存内容保存到 node_modules/.cache/babel-loader
目录,用户也可以通过 cacheDirectory = 'dir'
方式设置缓存路径。
2) 使用 cache-loader
除 babel-loader
、eslint-loader
这类特化 loader 自身携带的缓存功能外,Webpack 4 中还可以使用 cache-loader
实现与 Webpack 5 相似的通用持久化缓存功能,使用上只需将 cache-loader
配置在 loader
数组首位,例如:
const path = require("path");
const webpack = require("webpack");
module.exports = {
// ...
module: {
rules: [{
test: /\.js$/,
use: ['cache-loader', 'babel-loader', 'eslint-loader']
}]
},
// ...
};
cache-loader
文档:https://www.npmjs.com/package/cache-loader
使用 cache-loader
后,生产环境构建耗时从 10602ms 降低到 1540ms;开发环境构建从 11130ms 降低到 4247ms,性能提升约 「60% ~ 80%」
。
与 Webpack 5 自带的持久化缓存不同,cache-loader
仅 Loader 执行结果有效,缓存范围与深度不如内置的缓存功能,所以性能收益相对较低,但在 Webpack 4 版本下已经不失为一种简单而有效的性能优化手段。
参考
https://webpack.docschina.org/configuration/cache/#root