webpack 构建慢、产物大,应该怎样优化?
先用 speed-measure 之类的工具和 bundle 分析找到慢在哪、大在哪。提速主要靠持久化缓存(cache.type: 'filesystem')、缩小 loader 处理范围、用 esbuild 或 SWC 替代 babel-loader、多进程处理;减小体积靠 Tree shaking、代码分割、压缩,以及把大依赖换成更小的或按需引入。
简答
项目越写越大,
npm run build从几十秒变成好几分钟,打出来的主包好几 MB,这时要分两件事看:构建速度和产物体积。两者的优化手段不一样,但第一步都是先测量,不要凭感觉改配置。构建速度上,收益最大的通常是缓存。webpack 5 设置
cache: { type: 'filesystem' }后,会把编译结果缓存到磁盘,第二次构建只处理变化的模块。其次是让 loader 少干活:用include只处理src目录,不去转换node_modules;把babel-loader换成esbuild-loader或swc-loader,它们用 Go 和 Rust 实现,转换快很多。再往后可以用thread-loader把耗时的 loader 放到 worker 池里并行,但官方文档提醒,每个 worker 启动有开销,主进程和 worker 之间传数据也有成本,所以 worker 不要开太多,只给耗时很长的 loader 用。产物体积上,先用
webpack-bundle-analyzer看清楚包里是什么。常见的大头是整包引入的依赖,比如 lodash、moment、图表库,换成按需引入或更小的替代品;首屏用不到的页面和组件用动态import()拆出去;用splitChunks把第三方依赖单独打包,业务代码改了也不影响它的缓存;生产模式默认开启 Tree shaking 和压缩,要保证代码是 ES 模块写法,依赖声明了sideEffects。要注意,开发环境和生产环境的目标不同。开发时要的是启动和热更新快,可以关掉压缩、用便宜的 Source Map;生产时才需要完整的压缩和优化。
先测量再优化
构建速度可以用 speed-measure-webpack-plugin 看每个 loader 和插件各花了多久,或者用 webpack 自带的 --profile 参数。体积用 webpack-bundle-analyzer,它会生成一张图,每个模块占多大一目了然。
为什么一定要先测?因为构建慢的原因可能完全出乎意料,比如某个 loader 意外地处理了 node_modules 里的几千个文件,或者一个插件在每次构建时做了大量 I/O。不测就上 thread-loader,可能不仅没变快,还因为进程开销更慢了。
一份常见的优化配置
module.exports = {
mode: 'production',
cache: { type: 'filesystem' },
module: {
rules: [
{
test: /\.[jt]sx?$/,
include: path.resolve(__dirname, 'src'),
loader: 'esbuild-loader',
options: { target: 'es2018' },
},
],
},
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: { test: /[\\/]node_modules[\\/]/, name: 'vendor' },
},
},
},
};
cache.type: 'filesystem' 的缓存存在 node_modules/.cache/webpack 下。依赖或配置变了要让缓存失效,webpack 会自动追踪配置文件本身;如果构建还依赖别的文件,要在 cache.buildDependencies 里声明。CI 环境里每次都是全新的目录,要把这个缓存目录保存下来,缓存才有用。
splitChunks 把 node_modules 里的代码拆成单独的 vendor 包。第三方依赖很少变,文件名里的哈希也就不变,用户再次访问时可以直接用浏览器缓存,只需下载变化的业务代码。不过所有依赖打成一个包可能很大,实际项目常再把大的库单独拆出来。
常见追问
为什么 Tree shaking 没有生效?
常见原因是代码被转成了 CommonJS,比如 Babel 配置里把
modules设成了commonjs;或者依赖没有声明sideEffects: false,webpack 不敢删;或者是export default一个大对象,用到其中一个属性也会把整个对象留下。详见《Tree shaking 如何工作?为什么有些代码无法移除?》。
DllPlugin 现在还需要吗?
多数项目可以先不用。DllPlugin 把不常变的依赖放到单独的编译里预先打包,webpack 的构建性能文档仍然列出了它,并说明它能提高编译速度,但会让构建流程更复杂。webpack 5 的持久化缓存也能让二次构建跳过没变的模块,配置简单得多,一般先开缓存,测量后仍然不够快再考虑 DllPlugin。
换成 Vite 是不是就不用优化了?
Vite 开发时不打包,启动和热更新的问题基本解决了。可是生产构建仍然要打包,产物体积的问题依然存在,依赖太大、没做代码分割,换了工具也一样大。