Webpack 性能优化:分包
默认情况下,Webpack 会将所有代码构建成一个单独的包,这在小型项目通常不会有明显的性能问题,但伴随着项目的推进,包体积逐步增长可能会导致应用的响应耗时越来越长。造成白屏的概率越来越大
什么是分包
默认情况下,Webpack 会将所有代码构建成一个单独的包,这在小型项目通常不会有明显的性能问题,但伴随着项目的推进,包体积逐步增长可能会导致应用的响应耗时越来越长。归根结底这种将所有资源打包成一个文件的方式存在两个弊端:
「资源冗余」:客户端必须等待整个应用的代码包都加载完毕才能启动运行,但可能用户当下访问的内容只需要使用其中一部分代码
「缓存失效」:将所有资源达成一个包后,所有改动 —— 即使只是修改了一个字符,客户端都需要重新下载整个代码包,缓存命中率极低
这些问题都可以通过对产物做适当的分解拆包解决,例如 node_modules 中的资源通常变动较少,可以抽成一个独立的包,那么业务代码的频繁变动不会导致这部分第三方库资源被无意义地重复加载。为此,Webpack 专门提供了 SplitChunksPlugin 插件,用于实现产物分包。
使用 SplitChunksPlugin
plitChunksPlugin 是 Webpack 4 之后引入的分包方案(此前为 CommonsChunkPlugin),它能够基于一些启发式的规则将 Module 编排进不同的 Chunk 序列,并最终将应用代码分门别类打包出多份产物,从而实现分包功能。
使用上,SplitChunksPlugin 的配置规则比较抽象,算得上 Webpack 的一个难点,仔细拆解后关键逻辑在于:
SplitChunksPlugin 通过 module 被引用频率、chunk 大小、包请求数三个维度决定是否执行分包操作,这些决策都可以通过 optimization.splitChunks 配置项调整定制,基于这些维度我们可以实现:
单独打包某些特定路径的内容,例如 node_modules 打包为 vendors单独打包使用频率较高的文件
SplitChunksPlugin 还提供配置组概念 optimization.splitChunks.cacheGroup,用于为不同类型的资源设置更有针对性的配置信息
SplitChunksPlugin 还内置了 default 与 defaultVendors 两个配置组,提供一些开箱即用的特性:
- node_modules 资源会命中 defaultVendors 规则,并被单独打包
- 只有包体超过 20kb 的 Chunk 才会被单独打包
- 加载 Async Chunk 所需请求数不得超过 30
- 加载 Initial Chunk 所需请求数不得超过 30
什么是 Chunk
Initial Chunk:基于 Entry 配置项生成的 Chunk
Async Chunk:异步模块引用,如 import(xxx) 语句对应的异步 Chunk
Runtime Chunk:只包含运行时代码的 Chunk
而 SplitChunksPlugin 默认只对 Async Chunk 生效,开发者也可以通过 optimization.splitChunks.chunks 调整作用范围,该配置项支持如下值:
字符串 'all' :对 Initial Chunk 与 Async Chunk 都生效,建议优先使用该值
字符串 'initial' :只对 Initial Chunk 生效
字符串 'async' :只对 Async Chunk 生效
函数 (chunk) => boolean :该函数返回 true 时生效
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'
}
])
// when there are many pages, it will cause too many meaningless requests
config.plugins.delete('prefetch')
// set svg-sprite-loader
config.module
.rule('svg')
.exclude.add(resolve('src/icons'))
.end()
config.module
.rule('icons')
.test(/\.svg$/)
.include.add(resolve('src/icons'))
.end()
.use('svg-sprite-loader')
.loader('svg-sprite-loader')
.options({
symbolId: 'icon-[name]'
})
.end()
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.splitChunks({
chunks: 'all',
cacheGroups: {
libs: {
name: 'chunk-libs',
test: /[\\/]node_modules[\\/]/,
priority: 10,
chunks: 'initial' // only package third parties that are initially dependent
},
elementUI: {
name: 'chunk-elementUI', // split elementUI into a single package
priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app
test: /[\\/]node_modules[\\/]_?element-ui(.*)/ // in order to adapt to cnpm
},
commons: {
name: 'chunk-commons',
test: resolve('src/components'), // can customize your rules
minChunks: 3, // minimum common number
priority: 5,
reuseExistingChunk: true
}
}
})
// https:// webpack.js.org/configuration/optimization/#optimizationruntimechunk
config.optimization.runtimeChunk('single')
}
)
}
最佳实践
- 尽量将第三方库拆为独立分包
- 保持按路由分包,减少首屏资源负载,异步加载
vue 这种单页面应用,如果没有路由懒加载,运用 webpack 打包后的文件将会很大,造成进入首页时,需要加载的内容过多,出现较长时间的白屏,运用路由懒加载则可以将页面进行划分,需要的时候才加载页面,可以有效的分担首页所承担的加载压力,减少首页加载用时。
vue 路由懒加载有以下三种方式
- vue 异步组件
- ES6 的 import()
- webpack 的 require.ensure()