同样是写JavaScript,Webpack性能为啥不会随着迭代劣化

序言

基准测试(Benchmarking)是一种测量和评估软件或系统性能的方法,通常涉及特定操作的执行时间、资源消耗(如 CPU、内存使用)、处理速度以及响应时间等。Webpack 这个主流的前端构建工具,也拥有相应的基准测试来监控性能变化,确保每次更新都不会引入性能退化。本篇文章将深入探讨 Webpack 的基准测试是如何设计与实现的。

Webpack 性能监测指标

Webpack 基准测试的代码仓库位于:github.com/webpack/ben... 。有关Webpack性能的各项数据生成的图表可以在此查看:webpack.github.io/benchmark

上图显示了过去一年内 Webpack 性能指标的变化情况。图中红色部分表示的是开发环境的首次构建时间,蓝色部分则对应开发环境的增量构建时间。众所周知,Webpack 5 引入了持久化缓存功能,可以显著缩短增量构建时间,这一变化在图中得到了直观的体现。

Webpack 跟踪了丰富的性能指标,主要分为以下三大类:

  1. 内存

    • rss memoryResident Set Size 的缩写,它表示 Node.js 进程当前占用的总物理内存量,涵盖了 C++ 与 JavaScript 对象、代码的内存,以及映射到 Node.js 进程上下文中的共享库等。是衡量进程内存消耗的一个综合指标,不单指 JavaScript 或堆内存,对于评估内存效率和监控内存泄露非常关键。若 rss memory 值异常上升,可能暗示内存泄露的问题。
    • heap memory 是 V8 为 Node.j s应用程序分配的堆内存量。
    • external memory 是 V8 管理之外的内存,例如 Node.js 的 C++ 层面和某些内置模块为了性能考虑而使用的内存。
    • array buffers memory 是分配给 Array Buffers 的内存量,它可以是位于 Node.js 堆内存内部的一部分,也可以是外部分配的内存,取决于 Buffer 的大小和创建方式。
  2. 性能

    • exec 记录启动 Webpack 进程到构建彻底完成的整个周期所耗费的时间。
    • stats 记录自 Webpack 内部 Compilation 开始构建直至构建完成所耗费的时间。
    • 其余的指标记录 Webpack 构建流程中各个阶段所耗费的时间。
  3. 输出文件大小

    • 主文件(main.js)未压缩前的体积。
    • 主文件经过 gzip 压缩处理后的文件大小。
    • 所有输出文件的累积大小。
    • 所有输出文件经过 gzip 压缩后的累积大小。

执行 Webpack 的基准测试

现在让我们实际来执行一下 Webpack 的基准测试,本地环境中执行 Webpack 基准测试的步骤如下:

  1. 将Webpack基准测试的仓库克隆到本地。
  2. 通过 yarn 命令安装必要的依赖。
  3. 执行 node bin/measure.js common-libs production-build 命令启动基准测试。

我们将 Webpack 的基准测试仓库克隆到本地,先执行 yarn 命令安装所有的依赖,然后我们执行 node bin/measure.js common-libs production-build 就执行了一次集成测试。

这个命令会针对存放在cases目录下的common-libs示例项目,在生产环境模式下进行一次构建。

测试完成后,基准测试的结果将在控制台中显示,如下所示:

json 复制代码
{
  stats: 15558,
  'heap memory': 331893376,
  'rss memory': 528400384,
  'external memory': 28280559,
  'array buffers memory': 27147350,
  'Compiler.make hook': 2979.764833,
  'Compiler.finish make hook': 0.041625,
  'Compiler.finish compilation': 134.863833,
  'Compiler.seal compilation': 12417.891208000001,
  'Compiler.afterCompile hook': 0.026292,
  'Compiler.emitAssets': 12.826708,
  'Compiler.emitRecords': 0.040083,
  'Compilation.compute affected modules': 0.129542,
  'Compilation.finish modules': 102.072583,
  'Compilation.report dependency errors and warnings': 32.396125,
  'Compilation.optimize dependencies': 122.252792,
  'Compilation.create chunks': 53.017834,
  'Compilation.compute affected modules with chunk graph': 0.0775,
  'Compilation.optimize': 235.980917,
  'Compilation.module hashing': 109.575417,
  'Compilation.code generation': 716.393125,
  'Compilation.runtime requirements.modules': 2.346666,
  'Compilation.runtime requirements.chunks': 0.270417,
  'Compilation.runtime requirements.entries': 0.714167,
  'Compilation.runtime requirements': 3.49275,
  'Compilation.hashing: initialize hash': 0.004,
  'Compilation.hashing: sort chunks': 0.035416,
  'Compilation.hashing: hash runtime modules': 1.899708,
  'Compilation.hashing: hash chunks': 1.707042,
  'Compilation.hashing: hash digest': 0.020958,
  'Compilation.hashing: process full hash modules': 0.00075,
  'Compilation.hashing': 3.924167,
  'Compilation.record hash': 0.01025,
  'Compilation.module assets': 0.856417,
  'Compilation.create chunk assets': 3.110208,
  'Compilation.process assets': 11145.818875,
  'FlagDependencyExportsPlugin.restore cached provided exports': 5.26725,
  'FlagDependencyExportsPlugin.figure out provided exports': 89.815959,
  'FlagDependencyExportsPlugin.store provided exports into cache': 5.139709,
  'InnerGraphPlugin.infer dependency usage': 7.993075,
  'SideEffectsFlagPlugin.update dependencies': 35.021167,
  'FlagDependencyUsagePlugin.initialize exports usage': 3.124542,
  'FlagDependencyUsagePlugin.trace exports usage in graph': 83.510583,
  'buildChunkGraph.visitModules: prepare': 23.878776,
  'buildChunkGraph.visitModules: visiting': 42.373583,
  'buildChunkGraph.visitModules': 42.971375,
  'buildChunkGraph.connectChunkGroups': 0.113375,
  'buildChunkGraph.cleanup': 0.01875,
  'SplitChunksPlugin.prepare': 0.0305,
  'SplitChunksPlugin.modules': 30.579792,
  'SplitChunksPlugin.queue': 0.015792,
  'SplitChunksPlugin.maxSize': 0.017875,
  'ModuleConcatenationPlugin.select relevant modules': 13.668375,
  'ModuleConcatenationPlugin.sort relevant modules': 1.418458,
  'ModuleConcatenationPlugin.find modules to concatenate': 101.090625,
  'ModuleConcatenationPlugin.sort concat configurations': 0.002292,
  'ModuleConcatenationPlugin.create concatenated modules': 63.561083,
  exec: 16459,
  'main.js size': 3351189,
  'main.js gzip size': 728295,
  'main.js.LICENSE.txt size': 4178,
  'main.js.LICENSE.txt gzip size': 1573
}

Webpack 基准测试的项目与场景

Webpack 的基准测试工作针对的项目均存放在 cases 目录中,目前涵盖6个不同的测试项目。每个项目都构成了一个独立的npm项目,并包括相应的 package.json 文件。

测试这些项目可以在不同的构建场景中进行,我们目前可以选择的场景位于scenarios目录中,具体如下:

  1. development-build - 开发环境下的完整构建,对应于执行webpack build --mode development
  2. development-rebuild - 开发环境下的增量构建,适用于文件变动后的快速构建,对应于执行webpack build --mode development --watch
  3. production-build - 生产环境下的构建,为最终部署优化,对应于执行webpack build --mode production

当我们执行 node bin/measure.js common-libs production-build 这个命令时,我们就选择了 common-libs 这个测试项目,进行了基于生产环境配置的基准测试。

基准测试流程详解

在初次接触基准测试时,我会误以为基准测试是一项简单的任务。然而,实际操作中,基准测试涉及多项复杂的挑战,如确保测试结果的可靠性和稳定性、选择可靠的测试案例以真实反映性能表现等。

进行基准测试的目的是利用这些测试在每次项目迭代中评估性能改进或劣化。如果测试结果存在较大波动,或者测试不能有效反映真实使用场景的性能,那么基准测试的可信度和实用性就会被质疑。

对于 Webpack,基准测试的具体方法和过程在 lib/measure.js 中定义。以下是执行基准测试的详细步骤:

  1. 确认测试场景与插件 :选择需要进行测试的场景(scenarios)和插件(addons),这些设置决定了 Webpack 构建时的配置和测试流程。
  2. 缓存清理 :运行 clearCaches() 函数来清空特定路径下的缓存,如 .cache.yarn/.cache 等目录,目的是确保测试的准确性和一致性。
  3. 依赖安装:根据预设的场景和插件配置安装依赖。
  4. 环境准备(setupsetup() 函数在自动化构建环境测试前,准备必要的文件和配置。它能够动态调整配置文件和插件列表,使 Webpack 的构建过程能够适应不同的测试场景和插件要求。
  5. 环境预热:在进行正式测试之前,对环境进行预热,确保测试结果的可靠性和准确性。
  6. 测试执行与数据收集:执行基准测试,并进行多轮运行来收集统计数据,确保测试结果的可靠性和准确性。

预热的重要性和执行原因

在 Webpack 的基准测试内,warmup() 函数负责执行预热过程,预热都是一个完整且独立的构建过程。此过程的主要目的是提前让测试环境达到一种与实际运行相近的状态,以便后续正式的基准测试能够生成更为精确且一致的性能数据。

js 复制代码
const warmup = async (options) => {
    const org = args;
    args = args.filter((a) => a !== "--watch");
    await run({
        ...options,
        verbose: true,
        progress: process.env.CI ? false : true,
    });
    args = org;
};

通过预热运行代码,我们可以确保访问的数据和模块尽可能地被缓存在内存中,这样当实际的基准测试执行时,可以减少这些不确定的开销对最终测试数据的影响。

基准指标的获取流程

为了测量性能并获取各项基准指标,Webpack 基准测试在配置环节中将一个专门的 Webpack 插件注入到项目的 webpack.config.js 中。这个插件的源码位于 lib/build-plugin.cjs 文件中,它订阅了 Webpack 编译器(compiler)的 done 钩子(hook),以在编译完成后通过 Webpack 的 Stats 对象来收集所需的性能指标。

Webpack 的 Stats 对象包含了编译过程中产生的所有信息。开发者可以通过这个对象访问如构建时间、资源大小、模块依赖关系、生成的错误和警告清单等诸多重要信息。它是 Webpack 编译过程的详细报告,被开发者广泛用于分析和优化构建结果。

然后将它们按照一定格式打印到控制台,打印的性能指标示例如下:

shell 复制代码
#!# stats = 15558
#!# heap memory = 331893376
...

Webpack 的基准测试通过 Node.js 的 spawn 函数创建一个新的进程来运行 Webpack 构建命令。为了从这个子进程中获取基准测试指标,会监听该进程的标准输出(stdout),然后从中筛选出包含特定格式(如上面的例子)的行来解析并抓取测量到的性能数据。

这其实就是一种最简单的进程间通信:

增量构建测试流程

增量构建测试是Webpack基准测试的一个关键场景,称为development-rebuild。在这种测试下,我们通过添加 --watch 参数来启动Webpack,在监听模式下,当任意 JavaScript 文件被修改,Webpack 将重新构建项目,而测试关注点是这次重新构建所消耗的时间。

为了测量增量构建性能,我们回到 lib/build-plugin.cjs 定义的 Webpack 插件。该插件除了订阅 done 钩子外,还订阅了 watchRun 钩子,以识别 Webpack 是否处于监听模式。当确认处于该模式时,插件将向标准输出流(stdout)发送一条特殊消息 #!# next。基准测试的控制进程检测到这一消息后,会修改一个 JavaScript 文件,触发 Webpack 的增量构建。

此过程会被循环执行若干次,目的是对增量构建进行适当的预热。在预热完成后,Webpack插件将向 stdout 发送 #!# start 以表示开始记录标准测试结果,并按之前的流程输出性能指标。

在每次输出 #!# start 标记及随后的性能指标之后,插件还会再次发送 #!# next 消息给控制进程,指示其修改文件以开始新一轮的增量构建测试。这一反复过程将持续进行直至完成设定次数的增量构建,之后基于这些轮次的数据计算平均值,确保得出稳定且准确的性能测量结果。

基准测试插件

在 Webpack 的基准测试体系中,你还可以为测试添加自定义的插件。若要在测试过程中使用某插件,可以通过在测试命令中加入 + 符号及其名称的方式实现。如执行以下命令 node bin/measure.js common-libs production-build+no-cache,这样我们就启用了名为no-cache的插件,该插件通过禁用 Webpack 的缓存功能,帮助评估不利用缓存时的构建性能。

这些插件被归档于 addons 目录下,每一个都是独立的 JavaScript 模块,允许导出函数或变量,用以在执行基准测试时实现具体的自定义行为和配置调整。

例如,no-cache插件位于addons/no-cache.js文件中,它的实现非常简洁:

js 复制代码
export const config = (content) => `${content}

module.exports.cache = false;
`;

此插件通过导出 config 方法,修改 Webpack 的配置文件 webpack.config.js。将 cache 设置为false,关闭 Webpack 的缓存功能。

基准测试插件为测试流程带来高度的可定制性。开发者可以结合实际需求,选用和组合各类插件以构造出不同测试案例。

相关推荐
腾讯TNTWeb前端团队1 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
范文杰4 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪4 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪4 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy5 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom6 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom6 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom6 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom6 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom6 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试