发布于 2026-01-06 2 阅读
0

为什么 node_modules 比宇宙还大?(不开玩笑,它为什么这么大?)

为什么 node_modules 比宇宙还大?(不开玩笑,它为什么这么大?)

问题

你是否曾经遇到过这样的情况:在典型的 Node 项目中,下载依赖项时,仿佛过了很久很久yarn (或者npm install对于一些自虐狂来说)?当然,你肯定遇到过!这还用说吗?:)

但为什么会发生这种情况呢?🤔 原因之一显然是像npm这样的包管理器在下载 Node 模块时效率低下。虽然使用像pnpm这样更好的包管理器可以解决这个问题,但根本原因在于 JS 依赖项通常比它们对应的依赖项要大。

为什么JS依赖项通常比Go、Rust、Java等其他语言的相应依赖项更大?

原因

原因很简单——像 Golang 这样的语言的依赖项是源代码加二进制文件,而 JS 的依赖项是 JS 文件加 CSS 资源加 HTML 资源等等。JS 文件的大小通常比二进制可执行文件大,因此典型的 JS 依赖项通常也比对应的依赖项大。但问题是,为什么 JS 依赖项不是二进制文件呢?要回答这个问题,首先我们需要了解 JS 和 Go 等其他语言是如何编译代码的。

在 Go 这样的语言中,整个项目的输出会借助 Go 编译器编译成一个二进制可执行文件。这个二进制可执行文件可以共享给任何人,让他们在自己的机器上运行。安装 Go 依赖项时,也会安装源代码,但由于 Go 语言本身功能丰富,支持很多东西,因此 Go 包通常外部依赖项较少,而且这些依赖项也静态链接到 Go 包的二进制文件。正因如此,即使安装了源代码,Go 包的体积仍然很小。

那么,一个JS项目的输出是什么呢?通常是一个名为bundle.jsindex.js的单个JS文件,其中包含了项目的全部JS源代码,并以高度精简的方式打包,可能还会捆绑一些CSS和HTML资源。这通常是使用Webpack等工具实现的。

现在,为了执行这个最小化捆绑文件中的 JS,我们使用像ChromiumNode.js中那样的V8 引擎之类的 JS 引擎,它使用一种称为即时 (JIT) 编译的编译技术来执行 JS 。

JIT 编译技术是指编译器仅在执行时(即运行时)编译源代码,而不是像其他语言那样提前编译。这意味着在执行过程中,编译器会解释一行代码,并立即编译和执行。大多数现代 JavaScript 引擎会使用诸如提前编译 (AOT)等技术进一步优化这一过程。

JIT(即时编译)并非JavaScript独有的技术。Java也使用JIT来执行生成的字节码,但问题在于JavaScript没有像Java字节码那样的中间形式。因此,JavaScript引擎需要直接使用JavaScript源代码进行JIT编译。

由于 JS 引擎会在执行时编译 JS 源代码,而编译的唯一前提是源代码必须用 JS 编写,因此依赖项也必须用 JS 编写,而不是二进制可执行文件。因为如果依赖项是可执行文件,JS 引擎就无法处理它们。正如我们刚才所读到的,JS 引擎只需要 JS 源代码,而且大多数现代引擎都只处理JS 文件,不处理其他任何文件。

因此,由于这个原因,node_modules 中的模块需要输出为 JS 文件,导致其体积增大,变得臃肿。

额外嗡嗡声

虽然仍然可以通过使用像node-gyp这样的工具,将用其他语言(例如 C/C++)编写的二进制文件动态链接为 Node.js 中的原生插件模块,从而执行这些二进制文件,但这种情况下的执行是由操作系统完成的,而不是由 JS 引擎完成的。

JS 生态系统发展迅速⚡️,所以或许有一天我们能够解决模块臃肿的问题,但在那之前,作为开发者,我们可以尽己所能为生态系统做出贡献!

工程快乐!!😎👍🏻

文章来源:https://dev.to/faizbshah/why-is-nodemodules-heavier-than-the-universe-no-seriously-why-is-it-so-big-12dl