Webpack 的优化瓶颈,主要是两个方面:
- Webpack 的构建过程太花时间
- Webpack 打包的结果体积太大
构建过程提速策略#
不要让 loader 做太多事情 —— 以 babel-loader 为例#
最常见的优化方式是,用 include 或 exclude 来帮我们避免不必要的转译,比如 Webpack 官方在介绍 babel-loader 时给出的示例:
module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
}
这段代码帮我们规避了对庞大的 node_modules 文件夹或者 bower_components 文件夹的处理。但通过限定文件范围带来的性能提升是有限的。除此之外,如果我们选择开启缓存将转译结果缓存至文件系统,则至少可以将 babel-loader 的工作效率提升两倍。要做到这点,我们只需要为 loader 增加相应的参数设定:
loader: 'babel-loader?cacheDirectory=true'
不要放过第三方库#
处理第三方库的姿势有很多,其中,CommonsChunkPlugin 每次构建时都会重新构建一次 vendor;出于对效率的考虑,我们更多是使用 DllPlugin。
DllPlugin 是基于 Windows 动态链接库(dll)的思想被创作出来的。这个插件会把第三方库单独打包到一个文件中,这个文件就是一个单纯的依赖库。这个依赖库不会跟着你的业务代码一起被重新打包,只有当依赖自身发生版本变化时才会重新打包。
用 DllPlugin 处理文件,要分两步走:
- 基于 dll 专属的配置文件,打包 dll 库
- 基于 webpack.config.js 文件,打包业务代码
以一个基于 React 的简单项目为例,我们的 dll 的配置文件可以编写如下:
const path = require('path')
const webpack = require('webpack')
module.exports = {
entry: {
// 依赖的库数组
vendor: [
'prop-types',
'babel-polyfill',
'react',
'react-dom',
'react-router-dom',
]
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js',
library: '[name]_[hash]',
},
plugins: [
new webpack.DllPlugin({
// DllPlugin的name属性需要和libary保持一致
name: '[name]_[hash]',
path: path.join(__dirname, 'dist', '[name]-manifest.json'),
// context需要和webpack.config.js保持一致
context: __dirname,
}),
],
}
编写完成之后,运行这个配置文件,我们的 dist 文件夹里会出现一个叫 vendor-manifest.json 的文件用于描述每个第三方库对应的具体路径。
随后,我们只需在 webpack.config.js 里针对 dll 稍作配置:
const path = require('path');
const webpack = require('webpack')
module.exports = {
mode: 'production',
// 编译入口
entry: {
main: './src/index.js'
},
// 目标文件
output: {
path: path.join(__dirname, 'dist/'),
filename: '[name].js'
},
// dll相关配置
plugins: [
new webpack.DllReferencePlugin({
context: __dirname,
// manifest就是我们第一步中打包出来的json文件
manifest: require('./dist/vendor-manifest.json'),
})
]
}
Happypack—— 将 loader 由单进程转为多进程#
大家知道,Webpack 是单线程的,就算此刻存在多个任务,你也只能排队一个接一个地等待处理。这是 Webpack 的缺点,好在我们的 CPU 是多核的,Happypack 会充分释放 CPU 在多核并发方面的优势,帮我们把任务分解给多个子进程去并发执行,大大提升打包效率。
HappyPack 的使用方法也非常简单,只需要我们把对 loader 的配置转移到 HappyPack 中去就好,我们可以手动告诉 HappyPack 我们需要多少个并发的进程:
const HappyPack = require('happypack')
// 手动创建进程池
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length })
module.exports = {
module: {
rules: [
...
{
test: /\.js$/,
// 问号后面的查询参数指定了处理这类文件的HappyPack实例的名字
loader: 'happypack/loader?id=happyBabel',
...
},
],
},
plugins: [
...
new HappyPack({
// 这个HappyPack的“名字”就叫做happyBabel,和楼上的查询参数遥相呼应
id: 'happyBabel',
// 指定进程池
threadPool: happyThreadPool,
loaders: ['babel-loader?cacheDirectory']
})
],
}
构建结果体积压缩#
文件结构可视化,找出导致体积过大的原因#
一个非常好用的包组成可视化工具 ——webpack-bundle-analyzer,配置方法和普通的 plugin 无异,它会以矩形树图的形式将包内各个模块的大小和依赖关系呈现出来。在使用时,我们只需要将其以插件的形式引入:
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
}
删除冗余代码#
基于 import/export 语法,Tree-Shaking 可以在编译的过程中获悉哪些模块并没有真正被使用,这些没用的代码,在最后打包的时候会被去除。Tree-Shaking 的针对性很强,它更适合用来处理模块级别的冗余代码。至于粒度更细的冗余代码的去除,往往会被整合进 JS 或 CSS 的压缩或分离过程中。
这里我们以当下接受度较高的 UglifyJsPlugin 为例,看一下如何在压缩过程中对碎片化的冗余代码(如 console 语句、注释等)进行自动化删除:
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
module.exports = {
plugins: [
new UglifyJsPlugin({
// 允许并发
parallel: true,
// 开启缓存
cache: true,
compress: {
// 删除所有的console语句
drop_console: true,
// 把使用多次的静态值自动定义为变量
reduce_vars: true,
},
output: {
// 不保留注释
comment: false,
// 使输出的代码尽可能紧凑
beautify: false
}
})
]
}
这段手动引入 UglifyJsPlugin 的代码其实是 webpack3 的用法,webpack4 现在已经默认使用 uglifyjs-webpack-plugin 对代码做压缩了 —— 在 webpack4 中,我们是通过配置 optimization.minimize 与 optimization.minimizer 来自定义压缩相关的操作的。
按需加载#
- 一次不加载完所有的文件内容,只加载此刻需要用到的那部分(会提前做拆分)
- 当需要更多内容时,再对用到的内容进行即时加载
当我们不需要按需加载的时候,我们的代码是这样的:
import BugComponent from '../pages/BugComponent'
...
<Route path="/bug" component={BugComponent}>
为了开启按需加载,我们要稍作改动。首先 webpack 的配置文件要走起来:
output: {
path: path.join(__dirname, '/../dist'),
filename: 'app.js',
publicPath: defaultSettings.publicPath,
// 指定 chunkFilename
chunkFilename: '[name].[chunkhash:5].chunk.js',
},
路由处的代码也要做一下配合:
const getComponent => (location, cb) {
// 核心方法
require.ensure([], (require) => {
cb(null, require('../pages/BugComponent').default)
}, 'bug')
},
...
<Route path="/bug" getComponent={getComponent}>
require.ensure(dependencies, callback, chunkName)
这是一个异步的方法,Webpack 在打包时,BugComponent 会被单独打成一个文件,只有在我们跳转 bug 这个路由的时候,这个异步方法的回调才会生效,才会真正地去获取 BugComponent 的内容。这就是按需加载。所谓按需加载,根本上就是在正确的时机去触发相应的回调。
References:
https://juejin.cn/book/6844733750048210957/section/6844733750102720526