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

进击的代码 2024-10-22 09:36:56

我写了很多 JavaScript,我喜欢 JavaScript。更重要的是,我已经积累了一套理解、优化以及调试 JavaScript 的技能,我不愿意轻易放弃这些技能。

所以,当我看到现在的潮流是把每个 Node.js 工具重写为像 Rust、Zig、Go 等“更快”的语言时,可能我自然会感到有些担忧。别误会,这些语言很酷!(我现在桌上就放着一本 Rust 书,而且我还曾为 Servo 项目 做过一些小贡献,只是为了好玩。)但归根结底,我在学习 JavaScript 的方方面面上投入了大量时间和精力,这无疑是我最为熟悉的语言。

我承认自己可能有偏见(也许在某种技能上投入过多)。但越想越觉得,我的怀疑并不仅仅是个人情感,还是基于一些客观的理由,接下来我会在这篇文章中详细阐述。

性能

我怀疑的一个原因是,我不认为我们已经穷尽了让 JavaScript 工具提速的所有可能性。Marvin Hagemeister 在 这篇文章 中很好地展示了 ESLint、Tailwind 等工具中仍然存在的性能优化空间。

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

大重写

我认为性能差距来自几个方面。首先是前面提到的“低垂的果实”——长期以来,JavaScript 工具生态系统的重点是构建可行的东西,而不是快速的东西。现在我们已经达到了一个饱和点,API 接口基本稳定,每个人都希望“同样的功能,但是更快”。因此,涌现出许多几乎可以替代现有工具的新工具:比如 Rolldown 替代 Rollup,Oxlint 替代 ESLint,Biome 替代 Prettier 等。

然而,这些工具并不一定因为使用了“更快”的语言而更快。它们可能只是因为 1)在编写时就考虑到了性能,2)API 接口已经稳定,所以开发者不需要花大量时间在设计上纠结。你甚至不需要写测试!直接使用前一个工具的测试套件即可。

在我的职业生涯中,我经常看到从 A 重写到 B 后速度提升,随后就有人宣称 B 比 A 更快。然而,正如 Ryan Carniato 指出 的那样,重写之所以更快,往往只是因为它是重写——你第二次做的时候学到了更多,关注了性能等问题。

字节码与 JIT

性能差距的另一部分来自浏览器免费提供但我们很少考虑的东西:如 字节码缓存 和 JIT(即时编译器)。

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

此外,如果某个函数被频繁执行,它会被进一步优化为机器码。这就是 JIT 的作用。

在 Node.js 的脚本世界,我们完全无法享受到字节码缓存的好处。每次运行一个 Node 脚本时,整个脚本都需要从头开始解析和编译。这也是 JavaScript 和非 JavaScript 工具之间性能差距的一个重要原因。

不过,多亏了 Joyee Cheung 的不懈努力,Node.js 现在也有了 编译缓存。你可以设置一个环境变量,立即加快 Node.js 脚本的加载速度:

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

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

至于 JIT,这也是大多数 Node 脚本无法真正受益的领域。你必须运行某个函数才能使其“热起来”,所以在服务器端,JIT 更有可能在长时间运行的服务器上生效,而不是一次性脚本。

而且 JIT 的作用不可小觑!在 Pinafore 项目中,我曾考虑用 Rust(Wasm)版本替换基于 JavaScript 的 blurhash 库,后来 发现 到第五次迭代时,性能差异已经消失。这就是 JIT 的威力。

也许有一天像 Porffor 这样的工具可以用于 Node 脚本的 AOT(提前编译)。不过目前,JIT 仍然是本地语言在 JavaScript 之上的一个性能优势。

我还要承认:使用 Wasm 比使用纯本地工具确实存在性能损失。这可能是本地工具在 CLI 世界风靡的另一个原因,但在浏览器前端不一定如此。

贡献与可调试性

我之前已经提到过,这是我对“全部用本地语言重写”运动的主要怀疑来源。

在我看来,JavaScript 是一种“工人阶级”的语言。它对类型非常宽容(这也是我不太喜欢 TypeScript 的原因之一),并且相比于 Rust 这样的语言,它更容易上手。由于浏览器的支持,JavaScript 的用户群体非常庞大。

多年来,JavaScript 生态系统中的库作者和库使用者大多使用 JavaScript。我认为我们往往忽视了这带来的便利。

首先,贡献路径更加顺畅。引用 Matteo Collina 的话:

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

如果 JavaScript 库作者开始使用与 JavaScript 不同(且更难)的语言,这种情况就难以维持。那些作者简直像是半神!

其次,修改本地的 JavaScript 依赖项也很简单。我经常在本地 node_modules 文件夹中修改一些东西,以便追踪 bug 或者在我依赖的库中实现某个功能。如果使用的是本地语言编写的库,我需要检查源代码并自己编译——这无疑增加了门槛。

(公平地说,由于 TypeScript 的广泛使用,这已经变得有些棘手了。但 TypeScript 距离源 JavaScript 还不算太远,你会惊讶于在开发者工具中点击“美化”能走多远。幸运的是,大多数 Node 库也没有被压缩。)

当然,这也引出了调试性问题。如果我想调试一个 JavaScript 库,我可以简单地使用浏览器的开发者工具或我已经熟悉的 Node.js 调试器。我可以设置断点,检查变量,并像处理自己的代码一样分析代码。调试 Wasm 并非不可能,但需要完全不同的技能。

结论

我认为,JavaScript 生态系统的新一代工具非常棒。我对 Oxc 和 VoidZero 等项目的未来充满期待。现有的工具确实非常慢,可能会从竞争中受益。(我尤其对 eslint + prettier + tsc + rollup 的 lint+build 循环感到不满。)

尽管如此,我并不认为 JavaScript 天生就慢,或者我们已经用尽了改善它的所有可能性。有时,当我看到真正关注性能的 JavaScript 代码,诸如 最近对 Chromium 开发者工具 的改进,使用了像 将 Uint8Array 用作位向量 这样令人惊叹的技术时,我感觉我们才刚刚开始探索这个领域。(如果你真的想感受一下自卑感,可以看看 Seth Brenith 的其他提交。简直让人叹为观止。)

我还认为,作为一个社区,我们还没有真正思考过,如果我们将 JavaScript 工具交给一群精英的 Rust 和 Zig 开发者,世界会变成什么样子。我可以想象,普通的 JavaScript 开发者每次在他们的构建工具中遇到 bug 时,都会感到完全无助。与其说我们在赋能下一代 web 开发者,不如说我们可能在训练他们接受一种习得的无助。试想一下,面对一个 段错误 而不是熟悉的 JavaScript Error,普通的初级开发者会有怎样的感受。

到了我这个职业阶段,当然我没有什么借口继续抱着 JavaScript 这条“安全毯”不放。挖深几层,去理解技术栈的每一部分是我的职责之一。

然而,我忍不住觉得我们正在走上一条未知的道路,可能会产生意想不到的后果,而其实还有另一条风险较小的路径,几乎可以带来相同的效果。不过,目前这列高速列车似乎没有要减速的迹象,所以我猜我们到时候就会知道答案了。

0 阅读:0

进击的代码

简介:程序员,分享生活、工作、技术、学习。