前端打包优化实战:5个有效减小bundle体积的技巧
我的写法,亲测靠谱
打包优化这事儿,说大不大,说小不小。项目一上规模,首屏加载慢得像卡碟,用户直接关页面走人。我之前接手一个老项目,初始包快 3MB,gzip 后还有 800KB+,光 vendor 就占了一半。折腾了几天,最后压到 400KB 左右,Lighthouse 分数从 40 多拉到 85+。这里分享几个我反复验证过、在多个项目里跑通的实操方案。
首先,别迷信 SplitChunks 的默认配置。很多人直接用 Webpack 默认的 splitChunks.cacheGroups.vendor,结果把所有 node_modules 打成一个 chunk。看起来很整洁,但问题很大:只要更新任意一个依赖,整个 vendor 包就失效,缓存全废。我吃过这个亏。
我的做法是按需拆分高频、稳定的第三方库,比如 React、Lodash、Moment.js(虽然现在不推荐用了,但老项目逃不掉)。关键点是用 name + test 组合固定 chunk 名,这样哈希稳定,长期缓存才有效:
// webpack.config.js
optimization: {
splitChunks: {
cacheGroups: {
react: {
test: /[\/]node_modules[\/](react|react-dom)[\/]/,
name: 'vendor-react',
chunks: 'all',
enforce: true,
},
lodash: {
test: /[\/]node_modules[\/](lodash)[\/]/,
name: 'vendor-lodash',
chunks: 'all',
enforce: true,
},
// 其他稳定库同理
}
}
}
这样拆完,React 更新不会影响 Lodash 的缓存。而且小 chunk 并行加载更快,尤其对 HTTP/1.1 友好(虽然现在基本都是 HTTP/2 了,但兼容性不能忽视)。
这几种错误写法,别再踩坑了
我在 review 别人代码时,经常看到下面这些“看似聪明实则埋雷”的操作:
- 滥用动态 import 导致过度拆包:有人为了“按需加载”,把每个组件都写成
() => import('./Component')。结果首屏要加载十几个小 chunk,HTTP 请求爆炸,反而更慢。记住:动态 import 不是银弹,只对非首屏、低频使用的模块有效。首页核心组件该合并还得合并。 - 忽略 sideEffects 配置:很多库没标
sideEffects: false,Webpack 就不敢 Tree Shaking。比如你只用了 Lodash 的get,结果整个 Lodash 打进去了。解决方法有两个:一是手动指定路径import get from 'lodash/get';二是自己加 resolve.alias 强制指向子路径:// webpack.config.js resolve: { alias: { 'lodash': 'lodash-es', // 或直接指向具体模块 'moment$': 'moment/moment.js' // 避免加载 locale } } - Source Map 全开上线:开发时为了调试方便开了
sourceMap: true,结果忘了关,生产环境多出几百 KB 的 .map 文件。建议只在 staging 环境开,生产环境关掉,或者用hidden-source-map(只生成 map 文件但不引用,方便错误追踪)。
实际项目中的坑
有一次我优化一个后台管理系统,发现一个诡异现象:明明拆了 chunk,但 vendor 包还是巨大。查了半天,原来是某个内部 UI 库被当成 node_modules 打进去了——因为它放在 src/components/ui,但 package.json 里写了 "name": "@company/ui",Webpack 误判为外部依赖。解决方法是在 splitChunks 的 test 里排除自己的源码目录:
test: /[\/]node_modules[\/](?!@company[\/])/
另外,别忘了压缩 CSS 和图片。很多人只关注 JS,其实 CSS 未压缩能差 30%~50% 体积。我习惯用 css-minimizer-webpack-plugin + mini-css-extract-plugin,图片用 image-minimizer-webpack-plugin 跑 sharp 压缩。配置很简单:
// webpack.config.js
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin');
optimization: {
minimizer: [
'...', // 保留 JS 默认压缩器
new CssMinimizerPlugin(),
new ImageMinimizerPlugin({
minimizer: {
implementation: ImageMinimizerPlugin.sharpMinify,
options: {
encodeOptions: {
jpeg: { quality: 75 },
png: { quality: 80 },
},
},
},
}),
],
}
还有一点容易忽略:检查 Polyfill 是否冗余。如果你用 Babel + core-js,可能引入了大量浏览器已原生支持的 polyfill。建议配合 browserslist 配置,或者直接用 @babel/preset-env 的 useBuiltIns: 'usage' 按需注入。不过注意,usage 模式会增加构建时间,大型项目慎用。
核心代码就这几行
最后贴一个我常用的完整 optimization 配置骨架,删掉业务相关逻辑后可以直接复用:
// webpack.prod.js
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true, // 生产环境干掉 console
drop_debugger: true,
},
},
}),
// CSS 和图片压缩器如上
],
splitChunks: {
chunks: 'all',
cacheGroups: {
defaultVendors: false, // 关掉默认 vendor
vendorReact: {
test: /[\/]node_modules[\/](react|react-dom|scheduler|react-router)/,
name: 'vendor-react',
chunks: 'all',
enforce: true,
},
vendorUtils: {
test: /[\/]node_modules[\/](lodash|dayjs|axios)/,
name: 'vendor-utils',
chunks: 'all',
enforce: true,
},
default: {
minChunks: 2,
reuseExistingChunk: true,
},
},
},
runtimeChunk: 'single', // 把 webpack runtime 单独抽离,避免 vendor 变动
},
};
注意 runtimeChunk: 'single' 这一行很重要。它把 Webpack 的模块加载逻辑抽成独立 chunk,这样即使业务代码变了,runtime 不变,vendor 的缓存依然有效。
踩坑提醒:这三点一定注意
- 别盲目追求最小体积:有时候拆得太细,HTTP 请求增多反而拖慢加载。特别是移动端网络,延迟比带宽更致命。建议用 Webpack Bundle Analyzer 看图说话,找到平衡点。
- 测试真实设备:本地 dev server 快不代表线上快。一定要用 Chrome DevTools 的 Network Throttling 模拟 3G/4G,或者直接手机连热点测。
- 监控 bundle size 变化:在 CI 里加个脚本,如果主包体积增长超过 10%,自动 fail。我用过
webpack-bundle-analyzer --json生成报告,配合自定义脚本做对比,效果不错。
以上是我踩坑后的总结,希望对你有帮助。打包优化没有银弹,得结合项目实际反复调。改完后仍有一两个小问题也正常,比如某些老 IE 兼容性问题,但只要核心体验提升,就值得。有更好的方案欢迎评论区交流!

暂无评论