vue-cli 里面已经帮我们封装好了默认的 webpack 配置,但是不管在实际项目中或者面试过程都会遇到,使用 vue-cli 下还怎么对项目打包构建进行优化的问题。先基于做前端中后台用的比较多的框架vue-element-admin,去看它是怎么去做 webpack 优化的。
参考资料
webpack4 的变化
主要变化就是零配置,通过 mode 属性将开发和生产环境中常用的功能设置好默认值,拿来就可以使用。其中需要注意取消了常用于性能优化的 plugin,提供了一个名为 optimization 的配置项。
| 废弃项 | 替代项(optimization 属性) | 主要功能 |
|---|---|---|
| UglifyjsWebpackPlugin | sideEffects,minimize | 代码压缩 (tree shaking & mininzie) |
| ModuleConcatenationPlugin | opconcatenationModules | 作用域提升(scope hoisting) |
| CommensChunkPlugin | splitChunks,runtimeChunck,occurrenceOrder 等 | 代码分割(code splitting) |
| NoEmitOnErrorsPlugin | noEmitOnErrors | 编译出现错误,跳过输出阶段 |
表格参考 Webpack 4进阶。默认配置会在 production模式下,webpack 就会自动对代码进行分割、压缩、优化,这就是零配置的好处。
量化优化
webpack 优化主要是从构建速度和 bundle 体积大小这两个方向,我们需要引用插件来量化这两个指标。这里用到webpack-bundle-analyzer和 speed-measure-webpack-plugin,这两个插件可以看到打包花费的时间以及构建后的体积大小,这样在优化以后能看到一个比较明确的对比。
构建速度
下载插件以及 vue.config.js 配置:
npm i speed-measure-webpack-plugin -D
// vue.config.js
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin')
const smp = new SpeedMeasurePlugin({
outputFormat: 'human'
})
module.export = {
configureWebpack: smp.wrap({
// ...
})
}
配置完成之后执行打包指令的时候会看到详细的每个loaders和plugins的构建时间,这样就能够对构建时间进行一个量化。
bundle 体积
下载插件以及 vue.config.js 配置:
npm i webpack-bundle-analyzer -D
// vue.config.js
// ...
module.export = {
chainWebpack(config) {
// 可以限制只在 production 环境下才进行分析
// config.when(process.env.NODE_ENV === 'production', config => { })
config.plugin('webpack-bundle-analyzer').use(
require('webpack-bundle-analyzer').BundleAnalyzerPlugin
)
}
}
配置完成之后执行打包命令就会自动打开 localhost:8888,这里就可以看到 bundle 的大小以及具体的 code splitting 之后的分包情况,以便于我们对 bundle 进行量化分析。下图为在 development 模式下的打包情况。
优化分包策略
上面有说到 webpack4 使用 minimize 进行压缩,取消了 CommonsChunkPlugin,使用splitChunks进行分包,并且是有默认配置的,在不同模式下会有不同的分包策略(这里主要看production模式下的分包),所以我们要先清除 webpack 默认的分包策略是什么,再根据自己系统的业务需求进行分包优化。
默认策略
先使用默认配置进行打包查看 webpack 默认的分包策略,从下图可以看到:
- 入口文件依赖的文件全部被打包进 app.js
- 大于 30kb 的第三方包被单独打包成独立的 bundle
内置的代码分割策略:
- 新的 chunk 是否被共享或者是来自 node_modules 的模块
- 新的 chunk 体积在压缩之前是否大于 30kb
- 按需加载 chunk 的并发请求数量小于等于 5 个
- 页面初始加载时的并发请求数量小于等于 3 个
对于大部分应用来说,其实默认分包策略已经完成的不错,但是每个业务团队都会根据自己项目实际需求进行分包优化。比如讲共有组件抽出,对 app.js 打包的内容进行优化等等。
优化分包策略
基于自己项目的特点,在vue-element-admin的优化分包策略里面分析的很清楚,基于管理后台这个平台性质去进行分包策略的优化。我们可以学习到最主要的策略就是按照体积大小、共用率、更新频率重新划分包,使其尽可能的利用浏览器缓存。
根据文中的思路可以整理一些通用的分包策略思路:
-
基础类库 chunk-libs
vue 标准全家桶,升级频率不高,但是页面都需要的。这种可以单独进行一个打包。
-
UI 组件库 chunk-ui
不管是 ant-design 或是 element 打包出来的体积都过大,更新频率也会比基础类库要高一些。比如要更新 UI 组件库去修复一些官方的 BUG 或者是使用新的功能点。
-
自定义组件/函数 chunk-commen
一些全局组件
-
业务代码
业务代码使用路由懒加载的方式,webpack 默认会一个 view 一个包。
根据上面分包策略在vue.config.js中具体的代码为:
// vue.config.js
const path = require('path')
function resolve(dir) {
return path.join(__dirname, dir)
}
module.exports = {
chainWebpack(config) {
// ...
config
.when(process.env.NODE_ENV !== 'development',
config => {
config
.optimization.splitChunks({
chunks: 'all', // 对所有 chunk 都应用分包策略
cacheGroups: {
libs: {
name: 'chunk-libs', // 基础类库
test: /[\/]node_modules[\/]/,
priority: 10, // 切分的优先级
chunks: 'initial'
},
ui: {
name: 'chunk-ui', // ui库
priority: 20, // 切分的优先级,使 element-ui 不会被切分到 chuck-libs 中
test: /[\/]node_modules[\/]_?element-ui(.*)/
},
commons: {
name: 'chunk-commons', // 公共组件
test: resolve('src/components'),
minChunks: 3, // 最小共用次数
priority: 5,
reuseExistingChunk: true
}
}
})
}
)
}
}
optimization.splitChunks具体的配置可以在 webpack 官方文档 查看。这里主要对出现的属性进行一个简单地解析,主要为了理解这些配置到底做了什么。
optimization.splitChunks
chunks
简单来说就是选择哪些 chunks 进行优化。
- all :对所有 chunk 都应用分包策略
- async(default):针对异步 chuck 应用分包策略
- initial:针对普通 chunk 应用分包策略
cacheGroups.{}.priority
进入哪个 chunk 的优先级,默认组的优先级为负,自定义组的优先级为0。
element-ui也是属于基础类库,配置中如果不设置 {chunk-ui}.priority = 20的话,element-ui则会被切分到chunk-libs中。
minChunks
拆分前必须共享模板的最小 chunks 数。默认值为1
这里主要是判断是否有多个 chunk 共享(引用)了这个 chunk,对于公共组件来说如果只有极少的 chunk 去引用,其实也是不需要进行单独拆分,分别丢到每个引用了他的 chunk 里面也是可以接受的。这需要根据业务或者具体的项目需求进行定制。
cacheGroups.{}.reuseExistingChunk
如果当前 chunk 包含已从主 bundle 中拆分出的模块,则它将被重用,而不是生成新的模块。这可能会影响 chunk 的结果文件名。
设置为false,则会创建一个包含公共模块的新 chunk,如果为 true则会复用 chuck。设置与否的结果名称有区别。如果你没有名字,也没有区别。
持久化缓存
这个需要对 http 缓存机制有了解,这个可以挖个小坑专门写一篇文章来一起学习 http 缓存。
其实我们在 webpack 上做的就是给资源产生一个 hash,在内容有所改动的时候更新 hash,如果没有改动 hash 则保持不变(则保证了文件名称),这样就很好地利用了缓存对页面加载进行了优化。
RuntimeChunk
webpack4 中的 optimization.runtimeChunk 配置更加简单,作用就是将包含 ****chunks 映射关系的 list 单独从app.js里提取出来,打包时生成一个体积很小 runtime.xxx.js 文件,用作映射其他 chunk 文件,目的是更新后,以较小的代价利用缓存,提升页面加载速度。
只要更改了内容,runtime.xxx.js中的 hash 值一定会发生变化,但是可以看到这个文件的大小只有几k,因此我们只要使用 ScriptExtHtmlWebpackPlugin将 runtime 文件内联进 html 文件里面,减少网络请求数量,达到前端优化的目的。
该插件需要在 html-webpack-plugin 插件初始化后运行,还需要安装 html-webpack-plugin
npm i html-webpack-plugin script-ext-html-webpack-plugin -D
// vue.config.js
module.exports = {
chainWebpack(config) {
// ...
config
.when(process.env.NODE_ENV !== 'development',
config => {
config
.plugin('ScriptExtHtmlWebpackPlugin')
.after('html')
.use('script-ext-html-webpack-plugin', [{
// `runtime` must same as runtimeChunk name. default is `runtime`
inline: /runtime..*.js$/
}])
.end()
//...
config.optimization.runtimeChunk('single')
}
)
}
}
Preload / Prefetch
Prefetch
预提取是一种浏览器机制,浏览在下载或者预取用户可能要访问的文档。具体来说是通过 <link rel="prefetch" href="xxx.js"> 来实现预提取,设置 prefetch 的影响就是会降低首屏打开的速度,因为浏览器去需要加载这个资源。
在 webpack4 中是默认开启 preferch 的,在首屏 index.html中会把十几个页面路由文件,一口气下载下来,可以看到打包后的 index.html中加载了非常多没有意义的 js 和 css 文件。
为了提升首屏打开速度,一般会关闭 prefetch 这个功能,这样首屏只会记载当前页面路由的组件。当然如果真的有大概率即将被访问的资源也可以使用 prefetch 配置来提升性能和体验。
module.exports = {
chainWebpack(config) {
// ...
// when there are many pages, it will cause too many meaningless requests
config.plugins.delete('prefetch')
}
}
Preload
预下载也是使用<link rel="preload" href="xxx.js" as="script">来实现预下载,
- preload用来声明当前页面的关键资源,强制浏览器尽快加载;
- prefetch用来声明将来可能用到的资源,在浏览器空闲时进行加载
webpack 默认会预下载一些公共的 chunk 和首屏使用到的 chunk,我们可以去做些限制比如 runtime 等文件可以不用预下载的文件,以提升首屏打开速度。
module.exports = {
chainWebpack(config) {
// it can improve the speed of the first screen, it is recommended to turn on preload
config.plugin('preload').tap(() => [
{
rel: 'preload',
// to ignore runtime.js
// https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/cli-service/lib/config/app.js#L171
fileBlacklist: [/.map$/, /hot-update.js$/, /runtime..*.js$/],
include: 'initial'
}
])
}
}
这里就将 vue-element-admin 中的 webpack 优化全部学习完成,当然 webpack 优化除了这些方法以外还会有很多的方法进行优化。这个需要持续进行研究和学习。