1. 起因

老项目首屏加载一直被吐槽慢,用 webpack-bundle-analyzer 一看,主 bundle 将近 3MB,排查下来发现是几个很常见但容易被忽略的问题堆在一起,记一下排查思路和实际改动。

2. 先用分析工具看清楚体积都花在哪

1
npm install -D webpack-bundle-analyzer
1
2
3
4
5
6
7
8
// webpack.config.js
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

module.exports = {
plugins: [
new BundleAnalyzerPlugin(),
],
};

跑完之后会打开一个可视化页面,方块面积代表体积大小。当时一眼就看到几个问题:moment.js 占了将近 500KB(因为默认打包了全部语言包),lodash 被整体引入而不是按需引入,还有一个第三方图表库被重复打包了两份(一份在 node_modules 顶层,一份在某个依赖的嵌套 node_modules 里,版本号不一致导致没法去重)。不看这个图,光凭猜是很难定位到具体是哪个包的问题。

3. moment.js 语言包的坑

1
2
// 之前
import moment from 'moment';

moment 默认会把所有语言包一起打进去,哪怕项目只用中文。修法是用 webpack 自带的 IgnorePlugin 排除掉用不到的语言:

1
2
3
4
5
6
7
8
9
10
const webpack = require('webpack');

module.exports = {
plugins: [
new webpack.IgnorePlugin({
resourceRegExp: /^\.\/locale$/,
contextRegExp: /moment$/,
}),
],
};

需要哪个语言再手动 import 'moment/locale/zh-cn'。这一个改动直接省了 400KB 出头,是当时性价比最高的一步。(后来这个项目干脆把 moment 换成了 dayjs,体积从几百 KB 降到几 KB,如果是新项目,建议一开始就别选 moment。)

4. lodash 按需引入

1
2
3
4
5
6
7
// 之前:整个 lodash 都被打进去了
import _ from 'lodash';
_.debounce(fn, 300);

// 之后:只打包用到的函数
import debounce from 'lodash/debounce';
debounce(fn, 300);

或者用 babel-plugin-lodash 自动转换,不用每处手动改 import 路径。项目里当时只用了 debouncethrottlecloneDeep 三个函数,但完整引入 lodash 打包进去了整个库,这种”拿了个牛刀切个葱”的引入方式在老项目里特别常见,值得专门搜一遍代码里所有 import _ from 'lodash'

5. splitChunks 配置没生效

项目一开始也配了 splitChunks,但因为配置写得不对,实际没起作用:

1
2
3
4
5
6
7
8
9
10
11
// 之前,缺少 chunks: 'all',默认只对异步加载的模块生效
optimization: {
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
},
},
},
},

splitChunks.chunks 默认值是 'async',只会对通过 import() 动态引入的模块做分包,同步 import 引入的第三方库根本不会被这条规则命中,等于配置形同虚设。加上 chunks: 'all' 之后才真正把 node_modules 里的依赖单独拆出一个 vendor chunk:

1
2
3
4
5
6
7
8
9
10
11
12
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: -10,
},
},
},
},

拆出 vendor chunk 之后,业务代码变动时用户不需要重新下载没变化的第三方库部分,配合浏览器缓存,二次访问的加载速度提升更明显。

6. 小结

体积优化这件事最容易踩的坑不是”不知道怎么优化”,而是”没工具就靠猜”。webpack-bundle-analyzer 是第一步必做的事,看清楚体积具体花在哪之后,剩下的基本就是按需引入、干掉重复依赖、把 splitChunks 配置对这几件常规操作。这次优化下来主 bundle 从将近 3MB 降到 1.1MB 左右,首屏时间肉眼可见地变快了。