webpack构建原理
在正式开始前,我们先简单看一看 webpack
的打包构建过程:
- 从入口开始分析文件间的依赖关系;
- 调用 loader 对文件进行转换编译;
- 编译结束,根据依赖关系生成 chunk;
- 将编译完成的内容写入文件系统。
在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。
而针对 webpack
打包构建的优化,其实就是分析整个构建过程中可能存在的耗时操作、性能瓶颈等,再通过修改配置、使用工具(插件、loader)来提高速度,优化产出。
下面我们就来一起看看有哪些具体的优化手段吧!
下列相关代码、插件、配置等均以
webpack4
为例。另外所有插件、loader 的链接都是可点击的,方便小伙伴们快速跳转~
构建速度优化
工具
工欲善其事,必先利其器。
想要优化性能,我们就需要知道性能瓶颈在哪里。这里先给大家介绍一个构建速度分析工具:
SpeedMeasureWebpackPlugin
作用: 用于分析项目打包构建速度;
代码配置:
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin")
const smp = new SpeedMeasurePlugin({
// 输出文件格式化的方式
outputFormat: "humanVerbose",
// 输出最近两次构建速度的对比文件
compareLoadersBuild: {
filePath: "./buildInfo.json",
}
})
// 调用 wrap 方法 将 webpack 配置传入
const webpackConfig = smp.wrap({
plugins: [new MyPlugin(), new MyOtherPlugin()],
})
结果对比:
下图是引入 speed-measure-webpack-plugin
后,两次构建结果的分析对比;通过对比结果,我们就能很直观的看出究竟是哪些操作影响了构建速度:
开启多线程打包
针对 speed-measure-webpack-plugin
分析中,较为耗时的几个 loader ,我们可以借用插件开启多线程打包。
HappyPack
在 webpack 3 版本下最为常用的多线程打包插件,但目前已停止维护,因此不做过多介绍。
我们来简单看一下它的使用方法:
// 使用 5 个线程池
const happyThreadPool = HappyPack.ThreadPool({ size: 5 })
module.exports = {
...
plugins:[
new HappyPack({
id: 'eslintLoader', // 通过 id 标识作用的loader
loaders: [
{
loader:'eslint-loader'
}
],
threadPool: happyThreadPool, // 线程池配置
})
],
module: {
rules: [
{
test: /.(js|vue)$/,
loader: 'happypack/loader?id=eslintLoader', // 这里的 id=xxx 需要与上面声明的一致
enforce: 'pre',
include: [resolve('src'), resolve('test')]
}
]
},
}
thread-loader
webpack4
以后我们可以使用 thread-loader
来达到多线程打包的目的。
使用方法:将 thread-loader
放在需要使用多线程的 loader 之前;
// 如果有 watch 的需求,可以把 poolTimeout 设定为 Infinity 让 pool 一直存在增加编译效率
const jsWorkerPool = {
poolTimeout: Infinity
};
const cssWorkerPool = {
workerParallelJobs: 2,
poolTimeout: Infinity
};
// 通过预热线程池 提高线程启动的速度
threadLoader.warmup(jsWorkerPool, ['babel-loader']);
threadLoader.warmup(cssWorkerPool, ['css-loader', 'postcss-loader']);
rules: [
...
{
test: /.js$/,
include: [
path.resolve(__dirname, '../src')
],
use: [
{
loader: 'thread-loader',
options: jsWorkerPool
},
{
loader: 'babel-loader'
}
]
}
]
注意:开启多线程以及线程之间的通讯都会额外耗费时间,因此需要根据项目实际情况去使用,如果是体量比较小的项目使用后反而会降低打包速度。
合理利用缓存
针对一些耗费性能的 loader,可以通过使用缓存来提高其编译速度:
cache-loader
使用方法:在一些性能开销较大的 loader 之前添加此 loader,将结果缓存到磁盘里;
module.exports = {
module: {
rules: [
{
test: /.ext$/,
use: ['cache-loader', ...loaders],
include: path.resolve('src'),
},
],
},
};
babel-loader
babel-loader
往往也是造成性能瓶颈的一大原因。
它有一个 cacheDirectory
选项,默认值为 false
;当设置为 true
时,指定的目录将用来缓存 loader 的执行结果;之后的 webpack 构建,将会尝试读取缓存。
使用方法:
rules: [
...
{
test: /.js$/,
use: [
loader: 'babel-loader?cacheDirectory=true'
]
}
]
HardSourceWebpackPlugin
hardSourceWebpackPlugin
插件,通过第一次正常构建,并将结果写入缓存,二次构建时直接从缓存中读取编译结果,从而极大提高二次构建的速度;
使用方法:
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
module.exports = {
// ......
plugins: [
new HardSourceWebpackPlugin()
]
}
首次构建: 写入缓存,构建时间 49ms:
二次构建: 构建时间从 49ms => 3ms,有了大幅度的提升:
踩坑注意:
该插件目前已停止维护,偶发报错 Could not freeze 'xxxx'
:
如果在 webpack4 以下的版本中使用且出现这个报错,可以通过删除缓存目录(node_modules/.cache/hard-source) 解决。
webpack5
中提供了开箱即用的缓存机制,无需再额外使用该插件。
使用动态链接库
动态链接库的原理是把一些不常更新且构建时间较长的模块抽离,打包到一个个单独的动态链接库中去;当需要导入的模块存在于动态链接库中时,这个模块不会再次打包,而是直接去动态链接库中获取。
配置 dll 打包的过程较为繁琐,我们可以通过 autodll-webpack-plugin
来完成:
AutoDllWebpackPlugin
自动生成动态链接库。
const path = require('path');
const AutoDllPlugin = require('autodll-webpack-plugin');
module.exports = {
...
plugins: [
new AutoDllPlugin({
filename: '[name].dll.js',
context: path.resolve(__dirname, '../'), // 必须和 package.json 的同级目录
entry: {
// 配置要打包的文件
vendor: [
'vue',
'vue-router',
'vuex',
'moment',
'lodash',
'echarts'
]
}
})
]
}
未使用动态链接库打包结果:
使用动态链接库打包结果:
可以看到,对于打包构建速度的提升并不明显,这是因为 webpack4 对于这些模块的打包性能已经足够优秀了,因此收效甚微。
vue-cli 在早前的一次版本更新中,也将原有的
dll
option 去除了,具体可参照 issues。
缩小文件搜索范围
-
优化 loader 的 include/exclude
使用
include
去命中需要被 loader 转换的文件,缩小范围,还可以使用cacheDirectory
开启babel-loader
的缓存:loader: 'babel-loader?cacheDirectory'
-
优化 resolve.modules 配置
指明存放第三方模块的绝对路径,从而减少查找的层级。
-
优化 resolve.mainFields 配置
配置第三方模块的入口文件,减少搜索步骤。
-
优化 resolve.alias 配置
通过别名,把导入路径映射为完整的文件路径,可以减少 webpack 递归解析和处理的过程,但要注意,可能会影响 tree shaking。
-
优化 resolve.extensions 配置
文件后缀尝试列表,将尽量高频的后缀放在前面,不可能出现的后缀可以剔除。
-
优化 module.noParse 配置
忽略对没采用模块化的文件的递归解析处理。
产出代码的优化
介绍完了如何去提高 webpack
的打包构建速度,接下来我们把重点放在如何优化 webpack
打包构建后的代码上。
工具
首先还是给大家介绍一个打包产物的分析工具:
WebpackBundleAnalyzer
作用: WebpackBundleAnalyzer
插件能够用来分析 webpack
打包后的体积大小,并生成相关 html
文件报告。
使用方法:
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
}
可视化结果分析:
通过生成的报告(如下图),我们能够直观的分析打包后的文件信息,找出不合理的模块(如模块重复打包、常用/不常用模块被打包到一起等)。
知道了哪些模块的打包是不合理的,就能够针对性的去优化啦!
而一套合理的分包规则,不仅能够缩减打包后代码的体积,配合前端缓存去使用还能更多的命中缓存,提高网页性能,可谓一举多得!
具体优化策略,我们接着往下看:
SplitChunksPlugin
之前我们说到 webpack 会分析依赖,最终将文件打包成一个个 chunk, SplitChunksPlugin
就是用来告诉 webpack 应该遵循怎样的规则去生成 chunk。
SplitChunksPlugin
支持开箱即用,有一套默认拆分 chunks 的规则:
- 新的 chunk 可以被共享(至少使用了1次以上),或者模块来自于
node_modules
文件夹; - 新的 chunk 体积大于 30kb(在进行 min+gz 之前的体积);
- 当按需加载 chunks 时,并行请求的最大数量小于或等于 5;
- 当加载初始化页面时,并行请求的最大数量小于或等于 3;
当 chunk 满足以上默认规则时,将会被拆分出来。
合理分包
下面以我们的 vue
项目举例:
splitChunks: {
chunks: "all", // 同时处理同步和异步引入的模块
// 分包策略:
cacheGroups: {
// 1、vue 全家桶等基础类库,所有页面通用且升级频率不高,将这些单独打包
libs: {
name: "libs",
test: /[\/]node_modules[\/]/,
priority: 10, // 权重
chunks: "initial"
},
// 2、我们自己的UI库单独打包(体积较大,并且可能需要经常更新)
myUI: {
name: "myUI",
priority: 20,
test: /[\/]node_modules[\/]@myUI[\/]/
},
// 3、将echart单独打包
echarts: {
name: "echarts",
priority: 20,
test: /[\/]node_modules[\/]echarts[\/]/
},
// 4、将 src 目录下共享4次以上的模块打包到commons,
commons: {
name: "comomns",
test: path.resolve("src"),
minChunks: 4, // 模块至少应被4个chunk所共享才进行分割
priority: 5,
reuseExistingChunk: true
}
}
}
分包策略
这里给大家提供一份分包策略,大家在实际进行分包操作时可以参考:
类型 | 共用率 | 使用频率 | 更新频率 | 例子 |
---|---|---|---|---|
基础类库 | 高 | 高 | 低 | vue/react、vuex/mobx、xx-router、axios 等 |
UI 组件库 | 高 | 高 | 中 | Element-UI/Ant Design 等 |
必要组件/函数 | 高 | 高 | 中 | Nav/Header/Footer 组件、路由定义、权限验证、全局 State 、全局配置等 |
非必要组件/函数 | 高 | 高 | 中 | 封装的 Select/Radio 组件、utils 函数 等 (必要和非必要组件可合并) |
低频组件 | 低 | 低 | 低 | 富文本、Mardown-Editor、Echarts、Dropzone 等 |
业务代码 | 低 | 高 | 高 | 业务组件、业务模块、业务页面 等 |
分包策略来自文章 手摸手,带你用合理的姿势使用webpack4(下)
需要注意的是:项目的分包不能一味追求缩减包体积,要注意平衡包颗粒度与 HTTP
请求带来的消耗,针对自身项目多加尝试,找到较为合适的分包方式。
减少第三方库的体积
TreeShaking
Tree Shaking
能够帮助我们去除一些未使用的死代码,比如一些引入后未使用的模块、第三方库等。
Tree Shaking
依赖于 ES6 的模块语法(import export),因为 ES6 的模块语法是静态的,这使得 webpack 在打包过程中能够标记哪些模块没有被使用到,最终在 bundle 中删除它们。
使用 TreeShaking 正确姿势
-
正确的引入模块
使用导入具体的模块来代替全部导入。
// 全部导入 (不支持 tree-shaking)
import _ from 'lodash';
// 具名导入(支持 tree-shaking)
import { debounce } from 'lodash';
// 直接导入具体的模块 (支持 tree-shaking)
import debounce from 'lodash/lib/debounce';
-
使用支持 TreeShaking 的包
以 lodash 为例,官方有使用 common.js
语法的 lodash
,以及使用ES6模块语法导出的 lodash-es
版本,我们可以选用 lodash-es
版本。
这样一来,webpack 就能够通过这一特性帮助我们剔除死代码了。
忽略不需要的内容
以项目中的 moment.js
文件为例,其中的 locale.js
占据了很大一部分体积,而这个文件中包含的是世界各个国家的时区信息;
如果项目中未使用到这些内容,我们可以通过 IgnorePlugin 使用正则匹配忽略内容,或使用 NormalModuleReplacementPlugin 来将这些内容替换为空。
使用 IgnorePlugin
:
//webpack.config.js
module.exports = {
//...
plugins: [
new webpack.IgnorePlugin(/^./locale$/, /moment$/)
]
}
还可以直接使用替代的库 day.js,它与 Moment.js
的 API 设计保持完全一致,但是大小只有 2kb。
MiniCssExtractPlugin
在 webpack4 中,我们可以使用 MiniCssExtractPlugin
来将我们的CSS 提取到单独的文件中(webpack3 使用 ExtractTextWebpackPlugin
),该插件会为每个包含 CSS 的 JS 文件创建一个 CSS 文件。
作用:
- 支持 CSS 文件按需加载;
- JS 代码与 CSS 代码相互独立,互不影响缓存。
使用方法:
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
plugins: [new MiniCssExtractPlugin({
// 为了达到 JS 代码与 CSS 代码缓存互不影响的目的,这里需要使用 contenthash
filename: utils.assetsPath('css/[name].[contenthash].css'),
chunkFilename: utils.assetsPath('css/[id].[contenthash].css'),
ignoreOrder: true
})],
module: {
rules: [
{
test: /.css$/i,
use: [MiniCssExtractPlugin.loader, "css-loader"], // 在 css-loader 之后使用
},
],
},
};
压缩代码
TerserWebpackPlugin
该插件在 webpack5 内置,webpack4 版本仍需额外安装。
作用: 进行 js 代码压缩,从而缩小包体积。
使用方法:
const TerserPlugin = require("terser-webpack-plugin");
module.exports = {
optimization: {
minimize: true,
minimizer: [new TerserPlugin()],
},
};
CssMinimizerWebpackPlugin
作用: 搭配 MiniCssExtractPlugin
来实现优化和压缩 CSS 代码。
使用方法:
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
module.exports = {
module: {
rules: [
{
test: /.s?css$/,
use: [MiniCssExtractPlugin.loader, "css-loader", "sass-loader"],
},
],
},
optimization: {
minimizer: [
new CssMinimizerPlugin(),
],
},
plugins: [new MiniCssExtractPlugin()],
};
更多优化选择
- 通过
url-loader
将小图片转为 base64,减少 HTTP 请求; - 配置 externals,通过 CDN 引入第三方库;
- 使用 ImageMinimizerWebpackPlugin 压缩图片尺寸;
- ……
总结
以上就是 webpack
相关优化的全部内容啦!
说的再多都是纸上谈兵,各位小伙伴不妨在自己的项目上动手改造试试;毕竟实践出真知,相信上手完毕你的 简历又能多水一点 理解又会进一步加深!