性能

Webpack 本身的性能通常足以满足小型项目的需求。这也意味着,随着项目规模的扩大,它开始达到极限。这是 Webpack 中的常见问题。issue1905 就是一个很好的例子。

谈到优化,一般会有以下几个基本规则:

  1. 知道要优化的内容
  2. 首先从最快实现的部分开始
  3. 然后再调整其余部分
  4. 衡量影响

有时候,优化需要付出代价。它们抑或使您的配置更难理解又或者将其与特定解决方案联系起来。通常最好的优化是减少工作量或更智能的解决问题。我们将在下一节中介绍性能优化的基本原则,以便您知道何时应该关注性能。

衡量影响

如前一章所述,生成统计数据可用于衡量构建时间。speed-measure-webpack-plugin 为每个插件和 loader 提供更细粒度的信息,因此您可以了解哪些过程占用了大部分时间。

高级优化

默认情况下,Webpack 仅使用单个实例,这意味着您不做额外工作就无法从多核处理器中受益。针对此问题,有一些第三方解决方案,例如 parallel-webpackHappyPack

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.processfalse。要完全禁用它们,可以将 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 应用 includeexclude 规则。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。因为它们的上下文中可能没有包含 requiredefine,或者因为这会导致一个 Uncaught ReferenceError: require is not defined 错误。

总结

您可以通过多种方式优化 Webpack 的性能。在转向更复杂的技术之前,通常最好先使用更易于理解的技术。使用何种方法取决于您的项目。

回顾一下:

  • 从最能够更快实施的高层次的技术开始。
  • 较低层次的技术更加复杂但是获得结果也更好。
  • 由于 Webpack 默认使用单核运行,因此并行化是值得的。
  • 由于开发过程中是基于现代浏览器,跳过某些工作是可以接受的。

官方构建性能指南有更多提示。另请参阅让 Webpack 更快:更好的构建性能的指南我们如何将 Webpack 构建性能提高95%Webpack 优化 - 案例研究以及 Google 的 Web 基础知识

用户头像
登录后发表评论