Webpack 本身的性能通常足以满足小型项目的需求。这也意味着,随着项目规模的扩大,它开始达到极限。这是 Webpack 中的常见问题。issue1905 就是一个很好的例子。
谈到优化,一般会有以下几个基本规则:
- 知道要优化的内容
- 首先从最快实现的部分开始
- 然后再调整其余部分
- 衡量影响
有时候,优化需要付出代价。它们抑或使您的配置更难理解又或者将其与特定解决方案联系起来。通常最好的优化是减少工作量或更智能的解决问题。我们将在下一节中介绍性能优化的基本原则,以便您知道何时应该关注性能。
衡量影响
如前一章所述,生成统计数据可用于衡量构建时间。speed-measure-webpack-plugin 为每个插件和 loader 提供更细粒度的信息,因此您可以了解哪些过程占用了大部分时间。
高级优化
默认情况下,Webpack 仅使用单个实例,这意味着您不做额外工作就无法从多核处理器中受益。针对此问题,有一些第三方解决方案,例如 parallel-webpack 和 HappyPack。
parallel-webpack 并行运行多个 Webpack 实例
parallel-webpack 允许您以两种方式并行化 Webpack 配置。假设您已将Webpack 配置定义为数组,它可以并行运行配置。除此之外,parallel-webpack 还可以根据给定的变量生成构建。
使用变量允许您一次生成开发环境和生产环境两份打包结果。变量还允许您生成不同环境下的包,以便在特定环境下使用它们。变量可以与 DefinePlugin
配合使用,如“环境变量”一章中所讨论的那样。
我们还可以使用 worker farm 来实现这一想法。实际上,parallel-webpack 在底层依赖于 worker farm。
我们可以将 _parallel-webpack_作为开发依赖项安装到项目中,然后用 parallel-webpack
命令来替换 webpack
命令。
HappyPack - 文件级并行
与 parallel-webpack 相比,HappyPack 是一个更为复杂的选择。它基本原理是,HappyPack 拦截您设定的 loader 调用,然后并行运行它们。你必须先设置该插件:
webpack.config.js
...
const HappyPack = require("happypack");
...
const commonConfig = merge([{
{
plugins: [
new HappyPack({
loaders: [
// 捕获 Babel loader
"babel-loader"
],
}),
],
},
}];
要完成连接,您必须使用 HappyPack 替换原始 Babel loader 定义:
exports.loadJavaScript = ({ include, exclude }) => ({
module: {
rules: [
{
...
/* loader: "babel-loader", */
loader: "happypack/loader",
...
},
],
},
});
按照上面的配置,Webpack 就可以并行地运行 loader 了。HappyPack 还有更高级的用法,这里只是一个起点。
也许 HappyPack 的问题在于它将您的配置与它结合在一起。有可能通过设计克服这个问题并使注入更容易。一种选择是构建更高级别的抽象,可以在普通配置之上执行替换。
低层级优化
特定的低层级优化可能很有用,其中的关键就是让 Webpack 做更少的工作。在前面几章其实你已经实现了其中几个,这里我们较为完整地列举一下:
- 考虑在开发期间使用更快的源映射变体或跳过它们。如果您不以任何方式处理代码,则可以跳过源代码。
- 在开发过程中使用 @babel/preset-env 代替源映射,以便在现代浏览器减少特定的语法转换,并使代码更易读,更舒适。
- 在开发过程中跳过 polyfill。将诸如 @babel/polyfill 之类的包附加到应用程序的开发版本会增加开销。
- 禁用开发期间不需要的应用程序部分。编译您正在处理的一小部分可能是一个有效的想法,因为您可以减少打包尺寸。
- 减少基于 Node 的 polyfill 或者完全禁用它们。例如,一个包使用了 Node
process
,这样你的包就会变得很大。要禁用它,请设置node.process
为false
。要完全禁用它们,可以将node
直接设置为false
。请参阅webpack文档查看相关的默认值。 - 将不怎么变动的包制作成动态链接库(DLL)以避免不必要的执行过程。在官方的 WebPack 案例文章中谈到了这一点,同时 Rob Knight’s blog post 进一步解释了这种思想。autodll-webpack-plugin 可以自动制作动态链接库。
特定插件优化
还有一系列基于特定插件的优化:
- 可以使用 hard-source-webpack-plugin这样的插件来利用缓存,以避免不必要的工作。
- 在开发过程中使用功能相同但更轻量的插件替代品。我们可以使用 HtmlPlugin 替换
HtmlWebpackPlugin
。
特定 loader 优化
也可以对 loader 进行优化:
- 在开发过程中跳过某些 loader 的执行。特别是如果您使用的是现代浏览器,您可以完全跳过 babel-loader。
- 对于特定 loader 应用
include
或exclude
规则。Webpack 默认遍历_node_modules_ 并对其中的文件执行 babel-loader,除非我们额外进行配置。 - 使用 cache-loader 将花销较大的 loader(例如,图像处理)的结果缓存到磁盘。
- 使用 thread-loader 并行执行一些花销较大的 loader。鉴于 worker 在 Node 中存在一些开销,我们仅仅在并行任务较为繁重时才使用 thread-loader。
优化开发过程中重新打包的速度
在开发过程中,我们可以使用一些压缩版本的包,来优化重新打包的速度。比方说,React。 就 React 而言,你失去了基于 propType
的验证;但如果速度很重要,这种技术是值得的。
module.noParse
接受 RegExp 或 RegExps 数组。除了告诉 Webpack 不要解析你想要使用的压缩文件之外,你还必须使用 resolve.alias
指向 react
。在使用包章节中详细讨论了 resolve.alias
。
我们可以将上述想法封装成一个函数:
exports.dontParse = ({ name, path }) => {
const alias = {};
alias[name] = path;
return {
module: {
noParse: [new RegExp(path)],
},
resolve: {
alias,
},
};
};
我们可以按以下方式来调用该函数:
dontParse({
name: "react",
path: path.resolve(
__dirname, "node_modules/react/cjs/react.production.min.js",
),
}),
在此更改之后,应用程序应该能够更快地重建,具体取决于底层实现。该技术也可以应用于生产。
鉴于 module.noParse
接受一个正则表达式,如果你想忽略所有 *.min.js
的文件,你可以将其设置为 /\.min\.js/
。
并非所有模块都支持
module.noParse
。因为它们的上下文中可能没有包含require
、define
,或者因为这会导致一个Uncaught ReferenceError: require is not defined
错误。
总结
您可以通过多种方式优化 Webpack 的性能。在转向更复杂的技术之前,通常最好先使用更易于理解的技术。使用何种方法取决于您的项目。
回顾一下:
- 从最能够更快实施的高层次的技术开始。
- 较低层次的技术更加复杂但是获得结果也更好。
- 由于 Webpack 默认使用单核运行,因此并行化是值得的。
- 由于开发过程中是基于现代浏览器,跳过某些工作是可以接受的。
官方构建性能指南有更多提示。另请参阅让 Webpack 更快:更好的构建性能的指南, 我们如何将 Webpack 构建性能提高95%,Webpack 优化 - 案例研究以及 Google 的 Web 基础知识。