译:为什么我对用“更快”的语言重写 JavaScript 工具持怀疑态度

原文:https://nolanlawson.com/2024/10/20/why-im-skeptical-of-rewriting-javascript-tools-in-faster-languages/
作者:Nolan Lawson
译者:ChatGPT 4 Turbo

编者注:作者的原因有几点,1)沉默成本,他熟悉且喜欢 JavaScript,2)JavaScript 的性能并没有被穷尽,3)贡献和可调试性,JavaScript 是工人阶级语言,改用 Rust 或 Zig,用户遇到问题时会茫然。

我写了很多 JavaScript。我 喜欢 JavaScript。更重要的是,我在理解、优化和调试 JavaScript 方面积累了一套技能,我不愿意放弃这些。

所以,当我看到当前疯狂地用像 Rust、Zig、Go 等“更快”的语言重写每一个 Node.js 工具时,在我的 stomach 里感到一丝担忧也许是很自然的。别误会——这些语言很酷!(我现在桌上就有一本 Rust 书,我甚至为了好玩贡献了一点代码给 Servo。)但最终,我在学习 JavaScript 的各种细节上投入了大量的职业生涯,它是迄今为止我最舒适的语言。

所以我承认我的偏见(也许是过度投资于一项技能)。但我越想越觉得,我的怀疑也是由一些真实的客观担忧所支持的,我想在这篇文章中讨论这些担忧。

性能

我怀疑的一个原因是,我真的不认为我们已经穷尽了使 JavaScript 工具更快的所有可能性。Marvin Hagemeister 已经 出色地做到了这一点,他展示了在 ESLint、Tailwind 等工具中还有多少低效率的问题未被解决。

在浏览器世界里,JavaScript 已经证明自己对于大多数工作负载来说“足够快”了。当然,WebAssembly 存在,但我认为可以公平地说,它主要用于特定的、CPU 密集型任务,而不是用于构建整个网站。那么,为什么基于 JavaScript 的 CLI 工具急于抛弃 JavaScript 呢?

大重写

我认为性能差距来自几个不同的因素。首先,就是前面提到的低效率问题——很长一段时间,JavaScript 工具生态系统集中在构建一些 有效的 东西上,而不是快速的东西。现在我们已经达到了一个饱和点,API 表面大体上已经稳定,每个人都只想要“同样的东西,但更快。”因此,新工具的爆炸式增长几乎是现有工具的直接替代品:Rolldown 代替 RollupOxlint 代替 ESLintBiome 代替 Prettier 等。

然而,这些工具不一定更快,因为它们使用的是更快的语言。它们可能仅仅因为 1) 在编写时就考虑了性能,以及 2) API 表面已经稳定下来,所以作者不需要花时间去调整整体设计。哎呀,你甚至都不需要写测试!只需使用前一个工具的现有测试套件即可。

在我的职业生涯中,我经常看到从 A 到 B 的重写导致速度提升,随后伴随着 B 比 A 快的得意声称。然而,正如 Ryan Carniato 所指出的,重写往往只是因为它是重写 —— 第二次时你知道的更多,更加关注性能等等。

字节码和即时编译(JIT)

性能差距的第二类来自浏览器免费提供给我们的东西,这些东西我们很少考虑:字节码缓存JIT(Just-In-Time 编译器)

当你第二次或第三次加载一个网站时,如果 JavaScript 正确缓存,则浏览器不需要再将源代码解析并编译成字节码。它只是直接从磁盘加载字节码。这就是字节码缓存的作用。

此外,如果一个函数是“热”的(频繁执行),它将被进一步优化成机器代码。这就是 JIT 的作用。

在 Node.js 脚本的世界里,我们根本无法获得字节码缓存的好处。每次运行 Node 脚本时,整个脚本都必须从头开始解析和编译。这是 JavaScript 和非 JavaScript 工具之间报告的性能提升的一个重要原因。

不过,多亏了无与伦比的 Joyee Cheung现在 Node 正在获得一个编译缓存。你可以设置一个环境变量,立即获得更快的 Node.js 脚本加载速度:

export NODE_COMPILE_CACHE=~/.cache/nodejs-compile-cache

我已经在我所有的开发机器上的 ~/.bashrc 中设置了这个。我希望它有朝一日能成为默认的 Node 设置。

至于 JIT,这是另外一件不幸的事情,大多数 Node 脚本实际上无法真正从中受益。你必须运行一个函数之后它才会变得“热”,所以在服务器端,对于长时间运行的服务器而言,它更有可能启动,而不是一次性脚本。

而 JIT 可以带来很大的不同!在 Pinafore 中,我考虑过用 Rust (Wasm) 版本替换基于 JavaScript 的 blurhash 库,直到意识到 第五次迭代时性能差异被抹平。这就是 JIT 的力量。

也许最终像 Porffor 这样的工具可以用来对 Node 脚本进行 AOT (提前编译)。但与此同时,JIT 仍然是本地语言相对于 JavaScript 有优势的一个案例。

我还应该承认:使用 Wasm 相对于纯本地工具确实有性能损失。所以这可能是另一个原因,本地工具在 CLI 世界中风靡一时,但不一定是在浏览器前端。

贡献与可调试性

我之前暗示过,但这是我对“全部重写为本地”运动持怀疑态度的主要原因。

在我看来,JavaScript 是一种工人阶级语言。它非常宽容于类型(这是我不是很喜欢 TypeScript 的一个原因),它容易上手(与 Rust 等相比),而且因为它得到了浏览器的支持,有大量的人熟悉它。

多年来,我们在 JavaScript 生态中既有库作者也有库的消费者大量使用 JavaScript。我认为我们认为理所当然的是这使得什么成为可能。

首先:贡献的路径更加平滑。引用 Matteo Collina 的话:

大多数开发者忽略了一个事实:他们拥有调试/修复/修改依赖的技能。这些依赖并不是由一些神秘的大神维护,而是由和他们一样的开发者维护的。

如果 JavaScript 库的作者使用的是与 JavaScript 不同(并且更难!)的语言,这个情况就不成立了。他们可能确实像大神一样!

还有一点:在本地修改 JavaScript 依赖是很直接的。当我尝试跟踪一个库中的错误或在我依赖的库中开发一个特性时,我经常会在我的本地 node_modules 文件夹中调整点东西。而如果它是用本机语言写的,我就需要检出源代码并自己编译它 —— 这是一个很大的障碍。

(公平地说,由于 TypeScript 的广泛使用,这已经变得有点棘手了。但是 TypeScript 并不离原生 JavaScript 太远,所以你会惊讶地发现,通过在 DevTools 中点击“美化打印”,你可以走多远。幸运地是,大多数 Node 库也没有被压缩。)

当然,这也引导我们回到了可调试性问题上。如果我想调试一个 JavaScript 库,我可以简单地使用浏览器的 DevTools 或我已经熟悉的 Node.js 调试器。我可以设置断点,检查变量,并对代码进行推理,就像对待我自己的代码一样。这并非不可能通过 Wasm 实现,但它需要一套不同的技能。

结论

我认为 JavaScript 生态系统出现了新一代工具是很棒的事情。我很兴奋地看到像 OxcVoidZero 这样的项目将会走向何方。现有的竞争者确实非常慢,可能会从竞争中受益。(我特别讨厌典型的 eslint + prettier + tsc + rollup 的 lint+build 循环。)

也就是说,我不认为 JavaScript 固有地慢,或者我们已经用尽了所有改进它的可能性。有时候,当我看到像最近对 Chromium DevTools 进行的改进,使用像使用 Uint8Array 作为位向量这样令人震惊的技术,我觉得我们才刚刚触及到表面。(如果你真的想感到自卑,看看 Seth Brenith 的其他提交。它们很疯狂。)

我也认为,作为一个社区,我们还没有真正思考如果我们将 JavaScript 工具链委托给 Rust 和 Zig 开发者的精英圣职,世界将会是什么样子。我可以想象,每当构建工具出现一个 bug 时,普通的 JavaScript 开发者会感到完全无助。而不是赋予下一代 web 开发者更多的能力,我们可能在培训他们一种学到的无助感。想象一下,普通的初级开发者面对一个 segfault 而不是一个熟悉的 JavaScript Error 时会感觉如何。

此时,我在我的职业生涯中已经是资深人士了,所以当然我没有借口继续依赖我的 JavaScript 安全毯。搞清楚堆栈的每一部分如何工作是我的工作的一部分。

然而,我不禁感到我们正在踏上一条未知的道路,带来了意想不到的后果,当存在另一条较少风险、能够让我们获得几乎相同结果的道路时。当前的快车似乎没有减速的迹象,所以我猜我们到达那里时就会发现。