前言
看了很多打包优化的文章,很多都是基于原生的webpack配置,直接在webpack.config.js文件中修改配置的。但是vue-cli创建的项目已经封装了基本的webpack配置,需要在vue.config.js文件中修改预置的webpack配置。很少看到这方面的文章,因此记录一下自己的实践过程和踩过的一些坑。
本次使用技术的版本情况:
- vue:2.6.10
- @vue/cli:4.0.5
- webpack:4.31.0
vue-cli中的webpack
要优化项目,首先我们得了解vue-cli已经替我们做过了哪些优化,也就是需要查看webpack已经配置了哪些选项。
使用vue inpsect
输出webpack配置,还可以指定输出的文件:vue inspect > output.js
。
vue-cli提供了两种方式来更改webpack配置:
1、原生配置方式,配置的结果将会被 webpack-merge 合并入最终的 webpack 配置。
// vue.config.js
module.exports = {
configureWebpack: {
// 在这里直接书写webpack配置项...
}
}
2、链式配置方式,vue-cli内部是使用webpack-chain这个插件来维护webpack配置的,因为能更细粒度的控制其内部配置,因此也是官方比较推荐的一个方式。
// vue.config.js
module.exports = {
chainWebpack: config => {
config.resolve.alias.set('@assets', resolve(`src/assets`));
},
}
这两种方法可以配合使用。
为了简便,也为了少踩点儿坑,本次优化主要采用原生的webpack配置,也就是使用configureWebpack的方式。
优化过程分为打包体积优化和打包速度优化。
优化打包体积
使用webpack-bundle-analyzer分析打包体积
webpack官方提供一些插件分析打包性能。
- webpack-chart:webpack stats 可交互饼图。
- webpack-visualizer:可视化并分析你的 bundle,检查哪些模块占用空间,哪些可能是重复使用的。
- webpack-bundle-analyzer:一个 plugin 和 CLI 工具,它将 bundle 内容展示为便捷的、交互式、可缩放的树状图形式。
- webpack bundle optimize helper:此工具会分析你的bundle,并为你提供可操作的改进措施建议,以减少 bundle 体积大小。
我们使用webpack-bundle-analyzer来分析打包体积。
// yarn add analyze-webpack-plugin --dev
// vue.config.js
const AnalyzeWebpackPlugin = require('analyze-webpack-plugin')
module.exports = {
configureWebpack: {
plugins: [
new AnalyzeWebpackPlugin({}),
],
}
}
运行打包命令:yarn build
,会自动打开分析结果页面。

webpack-bundle-analyzer使用三种指标衡量打包体积:
- stat:输入的文件大小,还未经过例如压缩之类的转换。
- parsed:输出的文件大小,代码经过丑化压缩后的大小。
- gzip:开启了gzip压缩后的大小。
优化moment —— ContextReplacementPlugin
观察上图,可以发现moment占据了不小的比重,主要是一些本地化的语言包,默认都会打包进来。
对于普通应用来说,我们只需要中文语言包就够了。

优化前:
- Stat: 540.76KB
- Parsed: 234.36KB
- Gzipped: 68.46KB
首先选择合适的语言包设置语言环境:
import moment from 'moment';
import 'moment/locale/zh-cn';
moment.locale('zh-cn');
ContextReplacementPlugin插件的作用是改变某个模块的打包上下文,通过修改正则,来让webpack只打包我们想要的文件。
// yarn add webpack --dev
// vue.config.js
const webpack = require('webpack');
module.exports = {
configureWebpack: {
plugins: [
new webpack.ContextReplacementPlugin(
/moment[/\\]locale$/, // 这个参数表明了我们要改变的打包上下文
/zh-cn/ // 这个参数表示我们只想打包这个正则匹配的文件
)
]
},
};
优化后:
- Stat: 150.79KB
- Parsed: 54.61KB
- Gzipped: 17.96KB
原来393.36KB的语言包只保留中文后变为仅有3.39KB。

关于moment打包,社区提供了很多方法,还有其他一些方案可以参考:github.com/jmblog/how-…
优化XLSX
我们项目中使用了这个库来生成excel并下载。
原来直接引入import { utils, writeFile } from 'xlsx';
,打包后体积非常庞大。
优化前:
- Stat: 1.23MB
- Parsed: 920.85KB
- Gzipped: 327.65KB

后来在issue区查到了解决方案,改为只引入mini版本:
import { utils, writeFile } from 'xlsx/dist/xlsx.mini.min.js';
如果使用的是typescript会报错:

声明一下模块即可:
// modules.d.ts
declare module 'xlsx/dist/xlsx.mini.min.js';
优化后几乎只剩了零头:
- Stat: 236.73KB
- Parsed: 189.66KB
- Gzipped: 60.79KB

注意,官方解释这个xlsx这么大是有原因的,因为涉及到读取文件,要支持一些比较老的格式。如果你的项目中只是用来生成excel,不涉及读取文件,就可以用这个mini版本;如果有涉及到读取excel文件的操作,还是老老实实全量引入吧。官方未来或许会提供只支持现代文件格式的轻量级版本。
lodash打包体积 —— lodash专用plugin
优化前:
- Stat: 540.17KB
- Parsed: 73.29KB
- Gzipped: 25.74KB

需要使用两个插件:
-
babel-plugin-lodash 用来精简Lodash模块的,只保留用到的方法。
-
lodash-webpack-plugin 这个插件通过用noop, identity, 或其他更简单的替代品来替换一些模块的特性,使得打包后的体积更小(翻译)。
注意:这个插件默认会关闭一些lodash不常用的特性,可以给插件传递options来开启某些特性。
这两个插件配合使用来使效果最大化。只需要在Babel插件中添加lodash,并在webpack配置中添加一个插件:
// yarn add babel-plugin-lodash lodash-webpack-plugin --dev
// babel.config.js
modules.exports = {
// 其他配置省略...
plugins: ['lodash']
}
// vue.config.js
const LodashModuleReplacementPlugin = require('lodash-webpack-plugin');
module.exports = {
// ...
configureWebpack: {
plugins: [
new LodashModuleReplacementPlugin()
]
}
}
优化后:
- Stat: 56.42KB
- Parsed: 11.34KB
- Gzipped: 3.81KB
低调了许多,找了好久才找到 XD

抽取公共代码 —— splitChunks(webpack4之前使用commonChunkPlugin,webpack4之后使用splitChunks)
我们项目中使用了西瓜播放器,发现xgplayer作为第三方库,并没有被打包进chunk-vendors,并且还重复打包了两次。

关于这个xgplayer,引用情况是:有两个页面引用了一个公共的组件,这个组件引用了xgplayer。所以为什么xgplayer没有打包进chunk-vendors?
看一下vue-cli预设的webpack配置:
// ...
optimization: {
minimizer: [
// ...
],
splitChunks: {
cacheGroups: {
vendors: {
name: 'chunk-vendors',
test: /[\\/]node_modules[\\/]/,
priority: -10,
chunks: 'initial'
},
common: {
name: 'chunk-common',
minChunks: 1,
priority: -20,
chunks: 'initial',
reuseExistingChunk: true
}
}
}
}
vendors打包了node_modules里符合条件的第三方库,这个条件就是chunks: 'initial'
。
chunks表示要打包的这些chunks的类型,有三个值:
- initial:初始的chunk,需要立即加载,其实就是main.ts里通过import同步引入的模块。
- async:通过import()等动态引入的chunk,也就是按需引入的异步的模块。
- all:包含同步和异步的模块。选了这个,将会打包所有test匹配到的模块,这里是node_modules,显然是不合适的,因为有些第三方库可能晚点才会用到,比如这里的xgplayer。
所以xgplayer虽然是通过import同步引入的,但引用它的两个页面组件在路由文件中是import()按需引入的,并且没有在main.ts中引入xgplayer,所以自然不会打包到chunk-vendors里。
所以应该按照异步模块async或all的类型来打包。
// vue.config.js
module.exports = {
// ...
configureWebpack: {
optimization: {
splitChunks: {
cacheGroups: {
xgplayer: {
name: 'xgplayer',
test: /[\\/]node_modules[\\/]xgplayer[\\/]/,
minSize: 0,
minChunks: 1,
reuseExistingChunk: true,
chunks: 'all'
}
}
}
}
}
}
优化后只打包了一份:

优化echarts —— IgnorePlugin
优化echarts的难点在于,项目前期使用了两种方式:
- 引入了第三方的vue-echarts,参考这个组件写了一个自己的公共组件base-chart,结果并没有用到这个库。 页面的图表有的是基于base-chart的同时按需引入相关组件如echarts/lib/line。
- 有的是直接引用原生的echart从0开始写的组件。
这就导致了echarts全量引入,并且到处打包的问题。

解决方案:由于我们的首页是登录页,没有用到echarts,不需要第一次就加载echarts,因此要做两件事来优化:
- 这个第三方vue-echarts在main.ts中全局注册组件了,但其实并没有使用,需要删除全局引用,避免打包进chunk-vendors。
- 抽离重复打包的部分,合并进一个chunk。
// main.ts的组件注册代码也要注释掉
// import ECharts from 'vue-echarts';
// Vue.component('echart', ECharts);
optimization: {
splitChunks: {
cacheGroups: {
echarts: {
name: 'echarts',
test: /[\\/]node_modules[\\/]echarts[\\/]/,
minSize: 0,
minChunks: 1,
reuseExistingChunk: true,
chunks: 'all',
},
},
},
},
经过优化后已经从chunk-vendor里抽离出来,并把多处存在的echarts引用合并进了一个bundle。但是可以看到体积还是很大的。

github上有人就打包体积太大提了issue,作者建议使用在线builder,根据项目使用情况按需打包。并说5.0版本可能会考虑减小打包体积。

但是实际使用过程中打包到中途某些资源504网关超时了,重试了几次都失败,只好另寻他法。
使用webpack内置的IgnorePlugin插件来忽略项目中用不到的文件。可以对照在线builder的网址。
分别从node_modules/echarts/lib目录下的component、chart、coord三个目录进行排除。
IgnorePlugin插件配置项中,需要先使用contextRegExp来确定即将要排除的文件的上下文,这里是echarts目录。
然后使用resourceRegExp来指定要排除的资源的正则表达式。
实际上,这里只排除了这些目录,还有一些跟目录同级的文件,可能跟要排除的这些图表/组件相关,但是为了避免误判,就做不到那么精细了。
plugins: [
new webpack.IgnorePlugin({
resourceRegExp:
/^\.\/lib\/(component\/visualMap|toolbox|timeline|geo|brush|calendar)|(chart\/effectScatter|candlestick|heatmap|tree|treemap|sunburst|map|graph|boxplot|parallel|gauge|funnel|sankey|themeRiver|pictorialBar)|(coord\/polar|geo|singleAxis|calendar)$/,
contextRegExp: /echarts$/
})
]
优化后立马小了不少:

优化ant-design-vue
优化前:

发现icons占据很大的位置,但是实际使用的时候极少使用icons。
GitHub上面有人提了issue
作者解释说button会自动引用icon,设计如此。ant-design已经在优化了,目前暂时使用了作者推荐的方法来按需引入icon:
增加一个别名,让webpack解析的时候使用我们提供的icons.js文件中的路径,只打包使用过的icon。
resolve: {
alias: {
'@ant-design/icons/lib/dist$': resolve('./src/core/antd/icons.js')
}
},
然后在src目录下添加相应的文件,见github
export {
default as SettingOutline
} from '@ant-design/icons/lib/outline/SettingOutline'
export {
default as GithubOutline
} from '@ant-design/icons/lib/outline/GithubOutline'
export {
default as CopyrightOutline
} from '@ant-design/icons/lib/outline/CopyrightOutline'
/* MultiTab begin */
export {
default as CloseOutline
} from '@ant-design/icons/lib/outline/CloseOutline'
export {
default as ReloadOutline
} from '@ant-design/icons/lib/outline/ReloadOutline'
export {
default as DownOutline
} from '@ant-design/icons/lib/outline/DownOutline'
export {
default as AlignLeftOutline
} from '@ant-design/icons/lib/outline/AlignLeftOutline'
/* MultiTab end */
/* Layout begin */
export {
default as LeftOutline
} from '@ant-design/icons/lib/outline/LeftOutline'
export {
default as RightOutline
} from '@ant-design/icons/lib/outline/RightOutline'
export {
default as MenuFoldOutline
} from '@ant-design/icons/lib/outline/MenuFoldOutline'
export {
default as MenuUnfoldOutline
} from '@ant-design/icons/lib/outline/MenuUnfoldOutline'
export {
default as DashboardOutline
} from '@ant-design/icons/lib/outline/DashboardOutline'
export {
default as VideoCameraOutline
} from '@ant-design/icons/lib/outline/VideoCameraOutline'
export {
default as LoadingOutline
} from '@ant-design/icons/lib/outline/LoadingOutline'
export {
default as GlobalOutline
} from '@ant-design/icons/lib/outline/GlobalOutline'
export {
default as UserOutline
} from '@ant-design/icons/lib/outline/UserOutline'
export {
default as LogoutOutline
} from '@ant-design/icons/lib/outline/LogoutOutline'
/* Layout end */
优化后已经低调了许多:

至此,项目打包已经得到了很大程度的优化,对比优化前,打包的总体积减小了约1/3,压缩后减小了约一半的体积,终于降到了KB级,可喜可贺:
优化前:
- Stat: 10.54MB
- Parsed: 4.94MB
- Gzipped: 1.55MB
优化后:
- Stat: 7.57MB
- Parsed: 3.2MB
- Gzipped: 1003.36KB
总结
- 对于首页不需要的模块,尽量不要使用同步引用(import XX from '...')的方式引入到入口文件中,避免打包到一起,以减少首次请求的时间,加快首页的渲染速度;
- 使用第三方库的时候尽量按需引入,如果有需要,可以使用IgnorePlugin或ContextReplacementPlugin告诉webpack我们需要/不需要打包的文件;
- 使用splitChunks提取公共模块,注意chunks这个属性的值,如果是在按需引入(
import()
)的vue组件中使用同步引入的模块,chunks设置成initial
是没用的。这也是为什么vue-cli预设的splitChunks没有帮我们把某些重复代码抽离出来,它只会帮我们处理同步的模块:
// ...
optimization: {
minimizer: [
// ...
],
splitChunks: {
cacheGroups: {
vendors: {
name: 'chunk-vendors',
test: /[\\/]node_modules[\\/]/,
priority: -10,
chunks: 'initial'
},
common: {
name: 'chunk-common',
minChunks: 1,
priority: -20,
chunks: 'initial',
reuseExistingChunk: true
}
}
}
}
优化打包速度
使用speed-measure-webpack-plugin插件测量打包各环节耗费时间
vue-cli中的使用方法:
// yarn add speed-measure-webpack-plugin --dev
// vue.config.js
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
module.exports = {
// 这里无法使用链式写法chainWebpack,会报错
configureWebpack: smp.wrap({
// ... webpack config goes here ...
}
}
运行打包指令:yarn build
使用dll提取不常更新的公共库
更新中...