Webpack构建速度提升的5个实战优化技巧
构建慢到怀疑人生,打包5分钟起步
昨天上线前最后优化一波,本地改完代码保存,热更新直接卡10秒起,气得我差点把键盘扔了。这哪是开发,简直是渡劫。更离谱的是 build 一次要5分多钟,CI流水线跑着都快睡着了。忍不了,必须动手干它。
其实早几个月前就想过搞 Webpack 优化,但那时候项目小,build 40秒也能忍。现在加了一堆图表库、地图 SDK、富文本编辑器,还上了微前端,chunks 堆成山,vendor.js 直接飙到6MB,不优化真不行了。
先看问题在哪:不是玄学,是数据说话
第一步当然不能瞎改,得先分析瓶颈。我上 webpack-bundle-analyzer,这玩意儿谁没用过?经典可视化依赖图,一眼看出谁在占地方。
npm install --save-dev webpack-bundle-analyzer
// webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
// ... 其他配置
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: false,
reportFilename: 'bundle-report.html'
})
]
};
跑完 build,打开 report,好家伙,node_modules 占了85%,其中 lodash、moment、echarts 是三大巨头。尤其是 moment,带了所有语言包,实际只用了中文,纯浪费。
这里我踩了个坑:之前听说 externals 能把大包提到 CDN 上,立马动手把 echarts 和 vue 都 external 掉,结果 QA 环境报错,CDN 挂了页面直接白屏。后来想想太天真——公司内网环境根本没法稳定走外链,而且移动端弱网下雪上加霜。这个方案 PASS。
拆包策略:别让一个文件扛所有
接着试 code splitting。之前虽然开了 splitChunks,但一直用默认配置,等于没开。这次仔细调了下:
// webpack.prod.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\/]node_modules[\/]/,
name: 'vendors',
priority: 10,
reuseExistingChunk: true
},
echarts: {
name: 'chunk-echarts',
test: /[\/]node_modules[\/](echarts|zrender)[\/]/,
priority: 20,
chunks: 'async',
minChunks: 1,
minSize: 0
},
moment: {
name: 'chunk-moment',
test: /[\/]node_modules[\/](moment|moment-timezone)[\/]/,
priority: 20,
chunks: 'async',
minChunks: 1,
minSize: 0
}
}
},
runtimeChunk: {
name: 'runtime'
}
}
};
这里的重点是 priority 和 test 匹配要准。我一开始把 chunks: 'async' 忘了加,结果同步加载的模块也被拆了,首屏 JS 反而多了几个请求,LCP 更差了。折腾了半天发现,得控制只对异步 import 拆。
另外 minSize: 0 是为了确保哪怕很小也强制拆(比如 moment 就算只有几十KB我也想单独打)。但注意这会增加请求数,得结合 HTTP/2 来用,不然得不偿失。
moment 这种老古董怎么治?
moment 太重了,但业务里到处在用,短期没法替换成 dayjs 或 luxon。怎么办?插件救场:
// webpack.config.js
const webpack = require('webpack');
module.exports = {
plugins: [
// 只保留中文
new webpack.ContextReplacementPlugin(
/moment[/\]locale$/,
/zh-cn/
),
// 替代方案:也可以用 IgnorePlugin 干掉所有语言包
// new webpack.IgnorePlugin({
// resourceRegExp: /^./locale$/,
// contextRegExp: /moment$/
// }),
]
};
这一招直接砍掉 moment 里其他90%的语言包,体积从260KB干到30KB左右,效果立竿见影。不过要注意,如果以后真要加英文支持,得记得放开这个配置。
babel 也能压一压?
然后想到 babel 编译出来的代码是不是太“胖”了。我们用了 @babel/preset-env + useBuiltIns: 'usage',理论上已经按需引入 polyfill,但还是有点冗余。
后来试了下 babel-plugin-transform-imports,对 lodash 进行自动按需引入:
// .babelrc
{
"plugins": [
["transform-imports", {
"lodash": {
"transform": "lodash/${member}",
"preventFullImport": true
}
}]
]
}
配合这个,还得把原来 import { debounce } from 'lodash' 改成 import debounce from 'lodash/debounce',否则 preventFullImport 会报错。但这一步可以用 codemod 自动批量替换,我写了个 jscodeshift 脚本跑了一遍,省事。
HardSourceWebpackPlugin?快但不稳定
听说 hard-source-webpack-plugin 能缓存中间编译结果,提升二次 build 速度,立马加上:
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
module.exports = {
plugins: [
new HardSourceWebpackPlugin({
environmentHash: {
root: process.cwd(),
directories: ['node_modules'],
files: ['package-lock.json', 'yarn.lock']
}
})
]
};
第一次 build 会慢一点,第二次开始快了不少,本地 save 热更新从10秒降到3秒左右,爽。但问题来了:有时候改了 babel 配置没生效,清了 node_modules 重建也不行,最后发现是 hard-source 缓存没更新。只能手动删 node_modules/.cache/hard-source 目录,或者加个 npm script:
"scripts": {
"clean-cache": "rm -rf node_modules/.cache/hard-source"
}
所以这玩意儿适合稳定期用,开发频繁切分支的时候容易出诡异问题,建议非必要别上。
最终成果:build 从5分钟干到1分半
综合下来,最终优化点有这些:
- 精细化 splitChunks,大库单独拆包
- moment 语言包裁剪
- lodash 按需引入 + babel 插件辅助
- 启用 HardSource 缓存(开发环境)
- 图片资源上压缩(image-webpack-loader,略)
最终 production build 时间从 5m12s → 1m28s,gzip 后主包总大小从 6.8MB → 2.3MB,热更新响应时间从平均10s → 3s以内。虽然还没到理想状态,但至少能正常干活了。
顺带一提,后来发现 CI 上 build 慢主要是因为机器 IO 差,换了 SSD 实例后又快了20秒。所以说硬件也不能忽视……
还有两个小毛病没解决
改完后有个副作用:chunks 太多导致 index.html 里 script 标签一堆,看着闹心。试了 html-webpack-tags-plugin 自动注入倒还好,但加载顺序偶尔有问题。目前靠 initial / async 控制优先级勉强稳住。
另一个问题是某些异步组件 loading 态变多了,因为拆得太细,每个图表都单独 chunk,用户点一次才加载。这个后续打算做预加载:
// 在路由切换时提前 load
component.lazy().then(module => {
// 可以加个 preload hint
const link = document.createElement('link');
link.rel = 'prefetch';
link.href = '/static/chunk-echarts.js';
document.head.appendChild(link);
});
不过还没上,先观察数据再说。
以上是我踩坑后的总结
Webpack 优化真是一步步试出来的,没有银弹。你现在看到的这些配置,背后是我删了十几次 node_modules 重装换来的经验。有些方案看起来漂亮,实则线上翻车;有些土办法反而最稳。
如果你有更好的方案,比如用 ESBuild 或 SWC 做 loader 替代 babel,欢迎评论区交流。我最近也在看 Vite 迁移的可能性,但老项目改造成本高,暂时先拿 Webpack 挺着。

暂无评论