提升编译性能
1. 缩小资源搜索范围
使用
enhanced-resolve
缩小资源搜索范围;修改
resolve.extensions
减少匹配次数,代码中尽量补齐文件后缀名;resolve.modules
配置当 Webpack 遇到
import 'lodash'
这样的 npm 包导入语句时,会尝试先当前项目的node_modules
搜索资源,如果找不到则按目录层级尝试逐级向上查找node_modules
目录,如果依然找不到则最终尝试在全局node_modules
中搜索。在一个依赖管理执行的比较良好的业务系统中,我们通常会尽量保持
node_modules
资源的高度内聚,控制在有限的一两个层级上,因此 Webpack 这一逐层查找的逻辑大多数情况下实用性并不高,开发者可以通过修改resolve.modules
配置项,主动关闭逐层搜索功能:module.exports = {
//...
resolve: {
modules: [path.resolve(__dirname, 'node_modules')],
},
};tip注意该配置仅适用于 npm、yarn 等扁平化
node_modules
结构,由于 pnpm 是非扁平结构,启用该配置后会导致 Webpack 寻址出错,所有依赖的间接依赖都无法解析,只能配置shamefully-hoist=true
提升到根目录解决。限制依赖搜索范围,最合理的解决方案是用react-dev-utils
提供的ModuleScopePlugin
。实际上如果扁平化node_modules
结构,一律都到根目录node_modules
搜索也不合理,存在安装一个依赖的不同版本的情况,此时仍会嵌套安装,如果都到根目录搜索,只能解析到一个版本,容易出现版本不兼容问题。个人理解这边应该是限制模块解析范围到项目根目录。由于依赖管理复杂度,存在大量间接依赖,确实是无法避免逐层查找的问题,例如安装同一个依赖的不同版本,第二个依赖会被安装在某个依赖下的
node_modules
下,而不是项目根目录的node_modules
下。另外还存在peerDependencies
问题,例如antd
依赖react
,但是antd
本身不会安装react
,需要到项目根目录下node_modules
查找。resolve.mainFiles
配置与
resolve.extensions
类似,resolve.mainFiles
配置项用于定义文件夹默认文件名,例如对于import './dir'
请求,假设resolve.mainFiles = ['index', 'home']
,Webpack 会按依次测试./dir/index
与./dir/home
文件是否存在。因此,实际项目中应控制
resolve.mainFiles
数组数量,减少匹配次数。使用
Rule.oneOf
,一旦 loader 匹配成功就退出匹配,减少不必要的匹配次数注意
include
、exclude
条件问题const path = require('path');
module.exports = {
//...
module: {
rules: [
{
test: /\.css$/,
include: [
// will include any paths relative to the current directory starting with `app/styles`
// e.g. `app/styles.css`, `app/styles/styles.css`, `app/stylesheet.css`
path.resolve(__dirname, 'app/styles'),
// add an extra slash to only include the content of the directory `vendor/styles/`
path.join(__dirname, 'vendor/styles/'),
],
},
],
},
};实际上
exclude
还支持and
、not
表达式:module.exports = {
// ...
module: {
rules: [
{
test: /\.(js|ts)x?$/,
exclude: {
and: [/node_modules/], // Exclude libraries in node_modules ...
not: [
// Except for a few of them that needs to be transpiled because they use modern syntax
/unfetch/,
/d3-array|d3-scale/,
/@hapi[\\/]joi-date/,
]
},
use: ["babel-loader"],
}
]
}
}下面这段是一个实际业务工程的配置:
module.exports = {
module: {
rules: [
{
test: /\.(js|mjs|jsx|ts|tsx)$/,
// 解决 `react-hot-toast` 用模板字符串写 CSS 样式,导致无法压缩的问题
// 这里修改 exclude 让 babel-loader 处理 `react-hot-toast`
// 然后在 babel 配置里面加上 `@babel/plugin-transform-template-literals` 插件编译模板字符串
// 因为默认 target 配置不会降级到 ES5,需要手动配置插件
exclude: [{ and: [/node_modules/], not: [/react-hot-toast/] }],
loader: require.resolve("babel-loader"),
},
]
}
}
2. 跳过文件编译
有不少 npm 包默认提供了提前打包好,不需要做二次编译的资源版本,例如:
- Vue 包的
node_modules/vue/dist/vue.runtime.esm.js
文件 - React 包的
node_modules/react/umd/react.production.min.js
文件
对使用方来说,这些资源版本都是高度独立、内聚的代码片段,没必要重复做依赖解析、代码转译操作,此时可以使用 module.noParse
配置项跳过这些 npm 包:
module.exports = {
//...
module: {
noParse: /vue|lodash|react/,
},
};
配置该属性后,任何匹配该选项的包都会跳过耗时的分析过程,直接打包进 chunk,提升编译速度
配置后,所有匹配该正则的文件都会跳过前置的依赖分析动作,直接将内容合并进 Chunk,从而提升构建速度。不过,使用 noParse
时需要注意:
- 由于跳过了前置的 AST 分析动作,构建过程无法发现文件中可能存在的语法错误,需要到运行(或 Terser 做压缩)时才能发现问题,所以必须确保
noParse
的文件内容正确性; - 由于跳过了依赖分析的过程,所以文件中,建议不要包含
import/export/require/define
等模块导入导出语句 —— 换句话说,noParse
文件不能存在对其它文件的依赖,除非运行环境支持这种模块化方案; - 由于跳过了内容分析过程,Webpack 无法标记该文件的导出值,也就无法实现 Tree-shaking;
- 补充一点,
noParse
与externals
比较类似,Webpack 都不进行依赖分析,注意如果noParse
的文件同时还命中了 loader 规则,仍然会调用 loader 链进行构建。
业务项目使用 Webpack 打包,对于前端框架等基础性模块,配置 noParse
可以提升构建速度,但是注意不是所有第三方库都可以 noParse
,例如一些组件库,对于 peerDependencies
是不打包的,在构建产物中会保留 import
语句,也就是说需要在业务项目中进行打包。
如果第三方库将自身 peerDependencies
打包会出现什么问题?例如开发一个组件库,用到了 antd 的 Table
组件,如果该组件库将 Table
打包进去,业务项目引入该组件库,如果业务项目也用到了 antd 的 Table
组件,则会导致业务项目最终构建产物中存在两份 Table
组件的代码,导致出现模块冗余问题。
3. 最小化 Loader 作用范围
Loader 组件用于将各式文件资源转换为可被 JavaScript 理解、运行的代码片段,正是这一特性支撑起 Webpack 强大的资源处理能力。不过,Loader 在执行内容转换的过程可能需要做大量的 CPU 运算操作,例如 babel-loader、eslint-loader、vue-loader 等,因此开发者有必要根据实际需求,通过 module.rules.include
、module.rules.exclude
等配置项限定 Loader 的执行范围:
module.exports = {
// ...
module: {
rules: [{
test: /\.js$/,
exclude: /node_modules/,
// include: path.join(__dirname, './src'),
use: ['babel-loader', 'eslint-loader']
}]
}
};
示例配置
exclude: /node_modules/
属性后,Webpack 在处理node_modules
中的 js 文件时会直接跳过这个 rule 项,不会为这些文件执行后续的 Loader
一般来说,第三方库在发包的时候都会对源码进行构建,例如对 TS、JSX 语法进行语法转换,对 ES2015+ 语法进行编译兼容,然后整体再打包为一个 chunk(需要排除掉 peerDependencies
),这样业务项目引入该库,就不需要使用 Babel 进行耗时的编译转换了。
关于编译兼容这块需要注意一个问题,第三方库只建议进行语法转换,但不建议引入 polyfill。如第三方库单独引入 polyfill,业务项目引入该库打包的时候,很可能会导致模块冗余问题,因此建议只在业务项目中配置 useBuiltIns: "entry"
,在入口文件全量引入 polyfill。
4. 最小化 watch 监控范围
在 watch 模式下(通过 npx webpack --watch
命令启动),Webpack 会持续监听项目所有代码文件,发生变化时重新构建最新产物。不过,通常情况下前端项目中某些资源并不会频繁更新,例如 node_modules
,此时可以设置 watchOptions.ignored
属性忽略这些文件:
module.exports = {
//...
watchOptions: {
ignored: /node_modules/
},
};
5. 跳过 TS 类型检查
编译 TS 有以下几种方案:
- 使用
ts-loader
调用 tsc 进行编译、类型检查(适合小项目) - 使用
ts-loader
调用 tsc 进行编译,但不进行类型检查(配置transpileOnly: true
),使用fork-ts-checker-webpack-plugin
在单独的进程中进行类型检查 - 使用
babel-loader
调用 Babel 进行编译(需要安装@babel/preset-typescript
),可以复用 Babel 的 AST,使用fork-ts-checker-webpack-plugin
在单独的进程中进行类型检查
类型检查涉及 AST 解析、遍历以及其它非常消耗 CPU 的操作,会给工程化流程引入性能负担,必要时开发者可选择关闭编译主进程中的类型检查功能,同步用 fork-ts-checker-webpack-plugin
插件将其剥离到单独进程执行,例如对于 ts-loader
:
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
module.exports = {
// ...
module: {
rules: [
{
test: /\.jsx?$/, // 不仅编译 TS 文件,同时兼容 JS 文件
exclude: /node_modules/,
loader: resolve('babel-loader'),
options: babelConfig,
},
{
test: /\.tsx?$/,
use: [
{
loader: resolve('babel-loader'),
options: babelConfig,
},
{
loader: resolve('ts-loader'),
options: {
// 调用 tsc 进行编译,但不进行类型检查
// 应用 ForkTsCheckerWebpackPlugin 插件会自动开启该配置
transpileOnly: true,
},
},
],
},
]
},
plugins:[
new ForkTsCheckerWebpackPlugin()
]
};
这样,既可以获得 Typescript 静态类型检查能力,又能提升整体编译速度
6. 选择更加快速的 hash 函数
在 webpack 中,默认使用 md4
hash 函数,它将基于模块内容以及一系列元信息生成摘要信息。对于 hash 算法的一部分可参考 NormalModule
的 hash 函数。
_initBuildHash(compilation) {
const hash = createHash(compilation.outputOptions.hashFunction);
if (this._source) {
hash.update("source");
this._source.updateHash(hash);
}
hash.update("meta");
hash.update(JSON.stringify(this.buildMeta));
this.buildInfo.hash = /** @type {string} */ (hash.digest("hex"));
}
https://github.com/webpack/webpack/blob/main/lib/NormalModule.js
选择一个更加快速的 hash 函数,即可减少 CPU 消耗,并提升打包速度。比如将默认的 md4
换成 xxhash64
。Webpack v5.54.0+ 支持 xxhash64
作为更快的哈希算法。
https://webpack.js.org/configuration/output/#outputhashfunction
在 Webpack 中,可通过 output.hashFuction
来配置 hash 函数。
module.exports = {
entry: './index.js',
mode: 'none',
output: {
filename: 'main.[contenthash:6].xxhash64.js',
hashFunction: 'xxhash64'
}
}
Tip:这是面试问题如何提升 webpack 打包速度,八股文不常有的答案