JS桶装文件(Barrel Files)性能陷阱与优化指南

作者:极速慢跑

时间:2026-10-31

浏览:94

本文深入解析JS桶装文件(Barrel Files)对大型项目构建与测试速度的负面影响。通过对比实验,揭示模块图过大导致的解析耗时问题,并给出移除桶装文件以显著提升运行效率的优化建议。

文章目录

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() 语句并重新运行。结果再次证明,代码本身的执行速度并无问题。那么,为什么整体运行代码却如此耗时呢?

无需多言,这就是桶装文件对代码造成的破坏性影响。

收集更多信息

目前的关键信息是,代码的运行时并非瓶颈所在。我们测量发现,函数运行时间仅占总时间的极小部分。这意味着,剩余的时间主要消耗在代码运行之前。根据工具使用的经验,时间通常耗费在项目代码执行前的预处理阶段。

我们产生了一个大胆的想法:某些 npm 软件包出于性能考虑,会预打包其代码。这是否是一条新路径?我们决定测试该理论,并使用 esbuild 将代码打包为一个简单的文件。我们故意禁用了任何形式的压缩,以确保代码尽可能还原原始源码。

完成后,我们运行打包后的文件进行对照实验,结果令人惊讶:这次运行速度比之前快得多。出于好奇,我们测量了运行 esbuild 和打包后文件的总时间,发现累加后的时间仍比运行原始源码更快。这究竟是怎么回事?明明多了打包步骤,结果却比未打包的源码更快?

随后我们恍然大悟:打包器的超能力在于拍平和合并模块图。得益于 esbuild,原本由数千个文件组成的内容被合并为单一文件。这有力地证明,模块图的大小才是源码运行缓慢的根本原因。桶装文件正是导致速度低下的“万恶之源”。

剖析桶装文件

所谓“桶装文件”,指的是仅用于 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 语句外,没有包含任何代码。计时是在我的“量子计算机”上测量的。

JS 桶装文件

如你所见,加载更少的模块十分值得。让我们将这些数字应用到一个包含 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 桶装文件的资料请关注本站其它相关文章!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章