前面两篇文章《Webpack 性能系列二:多进程打包》、《Webpack 性能系列一: 使用 Cache 提升构建性能》已经详细探讨使用缓存与多进程能力,提升 Webpack 编译性能的基本方法与实现原理,这两种方法都能通过简单的配置极大提升大型项目的编译效率。
除此之外,还可以通过一些普适的最佳实践,减少编译范围、编译步骤提升 Webpack 性能,包括:
- 使用最新版本 Webpack、Node
- 配置 resolve 控制资源搜索范围
- 针对 npm 包设置 module.noParse 跳过编译步骤
- 配置 module.rules.exclude 或 module.rules.include 降低 Loader 工作量
- 配置 watchOption.ignored 减少监听文件数量
- 优化 ts 类型检查逻辑
- 慎重选择 source-map 值
下面会一一展开,解释每条最佳实践背后的逻辑。
一、使用最新版本
从 Webpack V3,到 V4,再到最新的 V5 版本,虽然构建功能在不断叠加增强,但性能反而不断优化提升,这得益于 Webpack 开发团队始终重视构建性能,在各个大版本之间不厌其烦地重构核心实现,例如:
V3 到 V4 重写 Chunk 依赖逻辑,将原来的父子树状关系调整为 ChunkGroup 表达的有序图关系,提升代码分包效率
V4 到 V5 引入 cache 功能,支持将模块、模块关系图、产物等核心要素持久化缓存到硬盘,减少重复工作
因此,开发者应该尽可能保持 Webpack 及 Node、NPM or Yarn 等基础环境的更新,使用最新稳定版本完成构建工作。
二、缩小资源搜索范围
Webpack 默认提供了一套同时兼容 CMD、AMD、ESM 等模块化方案的资源搜索规则 —— enhanced-resolve,它能将各种模块导入语句准确定位到模块对应的物理资源路径。
参考:https://github.com/webpack/enhanced-resolve
例如:
- import 'lodash' 这一类引入 npm 包的语句会被 enhanced-resolve 定位到对应包体文件路径 node_modules/lodash/index.js ;
- import './a' 这类不带文件后缀名的语句则可能被定位到 ./a.js 文件;
- import '``@/a' 这类化名路径的引用则可能被定位到 $PROJECT_ROOT/src/a.js 文件。
需要注意,这类增强资源搜索体验的特性背后涉及许多 IO 操作,本身可能引起较大的性能消耗,开发者可根据实际情况调整 resolve 配置,缩小资源搜索范围。
2.1 resolve.extensions配置
当模块导入语句未携带文件后缀时,如 import './a' ,Webpack 会遍历 resolve.extensions 项定义的后缀名列表,尝试在 './a' 路径追加后缀名,搜索对应物理文件。
在 Webpack 5 中,resolve.extensions 默认值为 ['.js', '.json', '.wasm'] ,这意味着 Webpack 在针对不带后缀名的引入语句时可能需要执行三次判断逻辑才能完成文件搜索,针对这种情况,可行的优化措施包括:
- 修改 resolve.extensions 配置项,减少匹配次数
- 代码中尽量补齐文件后缀名
- 设置 resolve.enforceExtension = true ,强制要求开发者提供明确的模块后缀名,这种做法侵入性太强,不太推荐
2.2 resolve.modules配置
类似于 Node 模块搜索逻辑,当 Webpack 遇到 import 'lodash' 这样的 npm 包导入语句时,会尝试先当前项目的 node_modules 搜索资源,如果找不到则按目录层级尝试逐级向上查找 node_modules 目录,如果依然找不到则最终尝试在全局 node_modules 中搜索。
在一个依赖管理执行的比较良好的业务系统中,我们通常会尽量保持 node_modules 资源的高度内聚,控制在有限的一两个层级上,因此 Webpack 这一逐层查找的逻辑大多数情况下实用性并不高,开发者可以通过修改 resolve.modules 配置项,主动关闭逐层搜索功能,例如:
- // webpack.config.js
- const path = require('path');
- module.exports = {
- //...
- resolve: {
- modules: [path.resolve(__dirname, 'node_modules')],
- },
- };
2.3 resolve.mainFiles配置
与 resolve.extensions 类似,resolve.mainFiles 配置项用于定义文件夹默认文件名,例如对于 import './dir' 请求,假设 resolve.mainFiles = ['index', 'home'] ,Webpack 会按依次测试 ./dir/index 与 ./dir/home 文件是否存在。
因此,实际项目中应控制 resolve.mainFiles 数组数量,减少匹配次数。
三、跳过文件编译
有不少 npm 包默认提供了提前打包好,不需要做二次编译的资源版本,例如:
- Vue 包的 node_modules/vue/dist/vue.runtime.esm.js 文件
- React 包的 node_modules/react/umd/react.production.min.js 文件
对使用方来说,这些资源版本都是高度独立、内聚的代码片段,没必要重复做依赖解析、代码转译操作,此时可以使用 module.noParse 配置项跳过这些 npm 包,例如:
- // webpack.config.js
- module.exports = {
- //...
- module: {
- noParse: /vue|lodash|react/,
- },
- };
配置该属性后,任何匹配该选项的包都会跳过耗时的分析过程,直接打包进 chunk,提升编译速度。
四、最小化 Loader 作用范围
Loader 组件用于将各式文件资源转换为可被 JavaScript 理解、运行的代码片段,正是这一特性支撑起 Webpack 强大的资源处理能力。不过,Loader 在执行内容转换的过程可能需要做大量的 CPU 运算操作,例如 babel-loader、eslint-loader、vue-loader 等,因此开发者有必要根据实际需求,通过 module.rules.include、module.rules.exclude 等配置项限定 Loader 的执行范围,例如:
- // webpack.config.js
- 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。
五、最小化 watch 监控范围
在 watch 模式下(通过 npx webpack --watch 命令启动),Webpack 会持续监听项目所有代码文件,发生变化时重新构建最新产物。不过,通常情况下前端项目中某些资源并不会频繁更新,例如 node_modules ,此时可以设置 watchOptions.ignored 属性忽略这些文件,例如:
- // webpack.config.js
- module.exports = {
- //...
- watchOptions: {
- ignored: /node_modules/
- },
- };
六、跳过 TS 类型检查
JavaScript 本身是一门弱类型语言,这在多人协作项目中经常会引起一些不必要的类型错误,影响开发效率。随前端能力与职能范围的不断扩展,前端项目的复杂性与协作难度也在不断上升,TypeScript 所提供的静态类型检查能力也就被越来越多人所采纳。
不过,类型检查涉及 AST 解析、遍历以及其它非常消耗 CPU 的操作,会给工程化流程引入性能负担,必要时开发者可选择关闭编译主进程中的类型检查功能,同步用 fork-ts-checker-webpack-plugin 插件将其剥离到单独进程执行,例如对于 ts-loader:
- const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
- module.exports = {
- // ...
- module: {
- rules: [{
- test: /\.ts$/,
- use: [
- {
- loader: 'ts-loader',
- options: {
- transpileOnly: true
- }
- }
- ],
- }, ],
- },
- plugins:[
- new ForkTsCheckerWebpackPlugin()
- ]
- };
- 参考:
- - https://github.com/TypeStrong/ts-loader#transpileonly
- - https://github.com/TypeStrong/fork-ts-checker-webpack-plugin
这样,既可以获得 Typescript 静态类型检查能力,又能提升整体编译速度。
七、慎用 source-map
source-map 是一种将经过编译、压缩、混淆的代码代码映射回源码的技术,它能够帮助开发者迅速定位到更有意义、更结构化的源码中,方便调试。不过,同样的 source-map 操作本身也有很大性能开销,建议读者根据实际场景慎重选择最合适的 source-map 方案。
针对 source-map 功能,Webpack 提供了 devtool 选项,可以配置 eval、source-map、cheap-source-map 等值,不考虑其它因素的情况下,最佳实践:
- 开发环境使用 eval ,确保最佳编译速度
- 生产环境使用 source-map,获取最高质量
参考:https://webpack.js.org/configuration/devtool/