JS生态系统加速桶装文件使用探索

 更新时间:2024年01月21日 11:00:34   作者:大家的林语冰 人猫神话  
这篇文章主要为大家介绍了JS 生态系统加速桶装文件使用实例探索,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

JS 桶装文件

长话短说:一大坨项目都塞满了只是重新 export 其他文件的文件。这就是所谓的“桶装文件”(barrel files),也是 JS 工具在大型项目中慢如龟速的关键原因之一。

本期《前端翻译计划》共享的是“加速 JS 生态系统系列博客”,包括但不限于:

  • PostCSS,SVGO 等等
  • 模块解析
  • 使用 eslint
  • npm 脚本
  • draft-js emoji 插件
  • polyfill 暴走
  • 桶装文件暴走
  • Tailwind CSS

本期共享的是第 7 篇博客 —— 桶装文件暴走。

新文件 import

假设我们正在开发一个包含一大坨文件的大型项目。我们添加一个新文件来处理新功能,并将另一个目录中的函数 import 到代码中。

import { foo } from './some/other-file'

export function myCoolCode() {
  // 假装这是超智能代码 :)
  const result = foo()
  return result
}

我们十分鸡冻地实现了功能,我们运行代码,但是发现这需要很久才能搞定。我们编写的代码一目了然,理论上应该不会太费时。为此,我们添加了某些测量代码,查看函数完成其任务所需的时间。

import { foo } from './some/other-file'

export function myCoolCode() {
  console.time()
  const result = foo()
  console.timeEnd()
  return result
}

我们重跑代码,大吃一斤的是,我们插入的测量结果表明,我们的函数速度惊人。我们重复测量步骤,但这次在项目的主入口文件中插入 console.time() 语句,然后再重跑代码。梅开二度,记录的测量结果只是反复证明我们的代码本身速度惊人。那为什么运行代码却很费时呢?

没时间解释了快上车。这就是桶装文件对代码造成的破坏性影响。

收集更多信息

目前为止,我们获得的关键信息是,代码的运行时不是 bug 所在。我们测量了函数,它只是运行总时间的九牛一毛。这意味着,我们可以假设,所有剩余时间耽误在运行代码前后。根据工具的经验,时间通常花在项目代码运行前。

我有一个大胆的想法:我们听过某些 npm 软件包出于性能考虑,会预打包其代码。也许这是一种新思路?我们决定测试该理论,并诉诸 esbuild 将代码打包到一个简单文件中。我们故意禁用任何形式的压缩,因为我们希望代码尽可能还原原始源码。

完事后,我们可以运行打包后的文件来对照实验,我和我的小伙伴都惊呆了,这次运行比猫猫还快。出于好奇,我们测量了运行 esbuild 和一起运行打包文件的时间,并注意到,它们累加起来仍然比运行原始源码更快。啊?到底是怎么回事?我们明明多了打包步骤,但运行结果却比未打包的源码更快?

然后我悟了:打包器的超能力是,拍平和合并模块图。得益于 esbuild,曾经由数千个文件组成的内容,被合并为有且仅有一个简单文件。这是一个有力的证明,模块图的大小是源码运行缓慢的 bug 所在。桶装文件乃速度低下的“万恶之源”。

剖析桶装文件

所谓“桶装文件”,指的是仅仅用于 export 其他文件,且本身不包含代码的文件。在编辑器支持自动 import 之前,一大坨开发者试图将它们必须手动编写的 import 语句的数量保持在最低限度。

// 瞄一眼所有这些 import
import { foo } from '../foo'
import { bar } from '../bar'
import { baz } from '../baz'

这就产生了一种模式,其中每个文件夹都有自己的 index.js 文件,该文件通常只是从位于同一目录中的其他文件重新 export 代码。某种程度上,这分摊了手动输入工作,因为一旦这样的文件就位,所有其他代码只需要引用一个 import 语句。

// feature/index.js
export * from './foo'
export * from './bar'
export * from './baz'

之前显示的 import 语句现在可以折叠成一行。

import { foo, bar, baz } from '../feature'

一段时间后,这种模式在整个代码库中蔓延,项目中的每个文件夹都有一个 index.js 文件。十分整洁,对不?对对对,才怪咧。

众生皆有病

在类似的设置中,模块大概率会 import 另一个桶装文件,该文件又双叒叕会拉入一大坨其他文件,然后 import 另一个桶装文件,子子孙孙无穷尽也。最终,我们通常会通过 import 语句的“蜘蛛网” import 项目中的每个文件。项目越大,加载所有这些模块就越久。

扪心自问:哪里快了?必须加载 30k 个文件更快,还是只加载 10 个更快?

JS 爱好者的“思想钢印”在于,模块只会按需加载。这大错特错,因为这样做会破坏依赖全局变量或模块执行顺序的代码。

// a.js
globalThis.foo = 123

// b.js
console.log(globalThis.foo) // 应该打印: 123

// index.js
import './a'
import './b'

如果引擎无法加载首个 ./a 导入,那么代码会意外打印 undefined 而不是 123

桶装文件的性能瓶颈

当我们考虑使用测试运行程序等工具时,情况会雪上加霜。在人气爆棚的 Jest 测试运行器中,每个测试文件都在其子进程中执行。实际上,这意味着,每个测试文件都从零开始构建模块图,并且必须支付该成本。如果在项目中构建模块图需要 6 秒,并且您只有 100 个测试文件,那么您总共会浪费 10 分钟,重复构建模块图。在此期间不会运行任何测试或其他代码。这正是引擎需要准备源码以便运行的时候。

桶装文件作为“性能杀手”的另一个法外之地是,任何类型的 import 周期 linting 规则。通常,linter 按逐个文件运行,这意味着,需要为每个文件支付构建模块图的成本。仅此一点就会导致 linting 时间爆表,并且在较大的项目中可能需要几小时,这种情况司空见惯。

为了获得某些原始数据,我生成了一个项目,其中的文件相互 import,以便更好地了解构建模块图的成本。每个文件都空空如也,除了 import 语句外,没有包含任何代码。计时是在我的“量子计算机”上测量的。

如你所见,加载更少的模块十分值得。让我们将这些数字应用到一个包含 100 个测试文件的项目,其中使用测试运行程序为每个测试文件生成一个新的子进程。我们的测试运行程序可以并行运行 4 个测试:

500 个模块:0.15s * 100 / 4 = 3.75s 开销

1_000 个模块:0.31s * 100 / 4 = 7.75s 开销

25_000 个模块:16.81s * 100 / 4 = ~7:00m 开销

50_000 个模块:48.44s * 100 / 4 = ~20:00m 开销

由于这是一个综合设置,因此这些数字都是低估的。在实际项目中,这些数字可能更糟糕。就工具性能而言,桶装文件并不友好。

我们该咋办?

代码中只有少量桶装文件通常问题不大,但当每个文件夹都有就会出现问题。不幸的是,这种灾难在 JS 行业屡见不鲜。

因此,如果大家从事的项目广泛使用桶装文件,那么可以免费优化:清空所有桶装文件,使一大坨任务速度加快 60-80%。

免责声明

本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考,英文原味版请传送 Speeding up the JavaScript ecosystem - The barrel file debacle[1]。

以上就是JS 生态系统加速桶装文件使用探索的详细内容,更多关于JS 桶装文件的资料请关注脚本之家其它相关文章!

相关文章

最新评论