JavaScript 二进制的 AST

2,841 阅读12分钟

JavaScript 二进制的 AST

在这个博客文章中,我想介绍一下 JavaScript 二进制 AST,我们希望在我们的项目中这将有助于使网页加载更快,以及其他一些好处。

背景介绍

多年来,JavaScript 已经从最慢的脚本语言之一,从老爷车发展为兰博基尼,不管是通过 Web 浏览器还是其他环境。它都能够快到可以运行桌面、服务器、移动甚至嵌入式应用程序。

随着 JavaScript 的增长,应用程序的复杂程度和规模都越来越复杂。然而,二十年前,少数使用过 JavaScript 的网站也就加载几千字节的 JavaScript,许多网站和非 Web 应用程序现在需要在用户开始实际使用之前加载几兆的 JavaScript 代码。

“几兆的 JavaScript 代码”听起来会很陌生,但是像 Steam 这样的本地应用程序只有 3.1 兆(纯二进制,没有资源,没有调试符号,没有动态依赖,在我的 Mac 上测量的结果)。Telegram 是 11 兆,Opera 更新程序 是 5.8 兆。因为浏览器实际上是动态依赖构建的,所以我并没算上 Web 浏览器的体积,但我估计Firefox 和 Chrome 有 100 余兆的大小。

当然,大型 JavaScript 源代码有几个成本,包括:

  • 重型网络传输;
  • 慢速启动。

我们现在已经能在很短的时间内解析 JavaScript 代码,在以前一个大型的 web 应用例如 Facebook 在一台较好的电脑上通过 500ms-800ms 的时间编译完成。几乎没有理由相信随着时间的推移,JavaScript 应用程序会变得越来越小。

因此,Mozilla 和 Facebook 的一个联合小组决定开始研究一种新的机制,我们相信通过二进制 AST 执行 JavaScript 可以极大地提高应用程序的速度。

二进制 AST 简介

JavaScript 二进制 AST 的思想很简单:我们可以通过发送二进制而不是发送文本源。

让我来澄清一下:二进制 AST 源码相当于文本的源码。并不是一个新的语言,也不是 JavaScript 的子集或超集,它 JavaScript。它不是一个字节码,而是源代码的二进制表示形式。如果您愿意,这个二进制 AST 就是一种专为JavaScript而设计的,并为了解析速度而优化过的源代码。我们还在构建一个可以提供可读的格式良好的源代码解码器。目前,这种形式并没有保留注释,但是有一个保留注释的提议。

生成一个二进制 AST 文件需要一个构建过程,我们希望这个过程越快越好。像 WebPack 或者 Babel 这样的构建工具会产生一个二进制的 AST 文件,因此,切换到二进制 AST 就像向构建传递一个标志一样简单,许多开发者已经开始使用。

我想在我未来博客的文章中详细介绍一些二进制 AST 的标准和我们的现状,现在,我来简述一下,早期的实验暗示我们可以能得到很好的源压缩和可观的解析速度。

我们已经研究二进制 AST 几个月了,现在项目已经作为 Stage 1 Proposal 被 ECMA TC-39 所接受。这是鼓舞人心的,但是还是需要一定的时间,你才能看到所有的 JavaScript 虚拟机和工具链的实现。

对比一下

和压缩格式对比

大部分的 web 服务器在发送 JavaScript 的时候已经使用了例如 gzip 或者 brotli 这样的压缩工具将 JavaScript 压缩了。这大大减少了等待数据的时间。我们在这里做的是一种专为 JavaScript 设计的格式。的确,在早期的原型内部使用 gzip,相比许多其他的技巧,我们早期的原型有两个主要优势:

  • 它使得解析速度更快;
  • 根据早期的实验,我们大幅度击败了 gzip 或 brotli。请注意,我们的主要目的是使分析速度更快,因此在未来,如果我们需要在文件大小和解析速度中做选择,我们最有可能选择更快的解析。另外,使用的压缩格式的内部可能会改变。

和压缩工具相比

web 开发者早期使用的用来减少 JS 文件大小的传统工具,例如 UglifyJS 和 Google’s Closure Compiler,这些工具称为压缩工具。

压缩工具通常移除未使用的空格和注释、修改变量然后缩短名称,并使用一些其他转换来使程序更短。

虽然这些工具确实有用,但它们有两个主要缺点:

  • 它们并不试图更快地进行解析 —— 事实上,我们已经目睹了在很多情况下,缩小意外使得解析更慢;
  • 它们有使 JavaScript 代码更难阅读的副作用,包括重命名不便于阅读的变量和函数,使用奇怪的特征将声明的变量打包,等等。

相反,使用二进制 AST 转换:

  • 用于使解析更快;
  • 以易于解码的方式保留了源代码并容易阅读所有变量名等。

当然,如果不希望保持源代码可读性的应用程序,混淆和二进制 AST 转换可以结合在一起。

和 WebAssembly 相对比

另一个令人兴奋的旨在提升确定的性能的 web 技术是 WebAssembly(wasm)。wasm 是为了使本地的应用被编译为一种格式,这种格式既可以有效地传输,也可以快速的解析,并通过 JavaScript 虚拟机以本地速度执行。

然而,设计者的意图是将 wasm 受限于本地代码,所以如果不是本地代码,JavaScript 将不起作用。

我不认为所有的 JavaScript 项目都可以通过 wasm 的编译。虽然这是可行的,但这将会是一项相当冒险的项目,因为这至少和开发一个新的 JavaScript 虚拟机的复杂度是相同的。同时还要确保仍然可以和 JavaScript 兼容(这是一个非常棘手的语言,并且每年至少生成一次说明文档或扩展),当然,如果生成的代码比今天的 JavaScript 虚拟机慢的话,这个任务就没用了,JavaScript 虚拟机现在越来越快了。并且如果编译之后的代码运行速度过慢或者文件太大,会使得启动非常慢(这也是我们在这里要解决的问题)或者使用编译的 JavaScript 库和(适用于浏览器应用程序的)DOM 导致无法工作。

现在,对这方面的探索绝对是一个有趣的工作,所以如果有人想证明我们错了,无论如何,请这样做:)

提高缓存

当 JavaScript 代码被浏览器下载时,它被存储在浏览器的缓存中,以避免以后再下载它。Chromium 和 Firefox 近期更新了他们的浏览器使得不仅 JavaScript 源文件可以缓存,字节码也可以加入缓存。因此,可以很好地解决页面再次加载的解析时间问题。我不知道 Safari 和 Edge 在这方面的进展,所以他们可能也会有类似的技术。

恭喜 Chromium 和 Firefox,这些技术都很棒!事实上,他们很好地提高重载页面的性能。这对于那些自从上次访问 JavaScript 代码但是没有更新的页面非常有效。

我们试图用二进制 AST 解决的问题是不同的,虽然一些页面是我们已经访问过并且经常访问的,但是还有更多的页面是我们只访问一次,哪怕是这个页面近期已经更新过了但我们并没有再访问。特别是,越来越多的应用程序得到非常频繁的更新 —— 例如,Facebook 每天发送几次新的 JavaScript 代码,并且 Twitter、LinkedIn、Google Docs 等情况也会类似。另外,如果你是一个 JS 的开发人员然后发布一个 JavaScript 应用程序 —— 无论是 Web 应用程序还是其他程序,你总是希望你和用户之间的第一次接触尽可能平滑,这意味着你希望第一个加载(或更新后的第一次加载)也非常快。

这些问题我们都可以使用 二进制 AST 解决。

假设

如果我们提高了缓存会怎样?

额外的技术是要使得浏览器提前抓取和预编译 JS 代码和字节码。

这些技术确实值得研究,也将有助于我们开发二进制 AST 脚本 —— 每一种技术都改进了另一种技术。特别是,当使用这种技术时,二进制AST的更好的资源效率将有助于限制资源浪费,同时也改善了这些技术根本不能使用的情况。

如果我们使用一个现有的 JS 字节码会怎样?

大多数(要不就是所有的)JavaScript 虚拟机已经使用一个内部的 JS 字节码。我似乎记得至少微软的虚拟机支持特殊的应用使用 JavaScript 字节。

所以,你可以想象一下浏览器厂商将他们的字节码开源并且使所有的 JavaScript 应用使用字节码。这样的话,听起来不是一个好主意,有以下几个原因:

第一:影响虚拟机的开发者。一旦你暴露自己的内部表示的 JavaScript,你注定要维护它。事实证明,JavaScript 字节码经常变化,以适应新版本的语言或新的优化。强迫虚拟机保持与旧版本的字节码的兼容性将是一个维护和/或性能灾难,所以我怀疑任何浏览器或 VM 供应商都愿意提交这个,除非在非常有限的设置中。

第二:影响 JS 开发者。有几个字节码就意味着维护和运送几个二进制,可能有几十个,如果你想要优化后续版本的浏览器的字节码。更糟糕的是,这些字节码会有不同的语义,导致不同语义的 JS 代码编译。虽然这是可能的,毕竟,移动和本地的开发者都是这样做的,这就是在回退 JavaScript。

我们有一个标准的 JS 字节码会怎样?

所以,如果 JavaScript 虚拟机开发者决定想出一个新的字节码格式,可能作为一个扩展 WebAssembly,但专为 JavaScript 设计呢?

要明确一点:我听到有人后悔没有开发一个这样的格式,但我不知道有人积极致力于此。

没有人这样做的原因是设计和维护一种随时变化的语言的字节码是相当复杂的,对于一种像 JavaScript 这种已经很复杂的语言来说,将会更加复杂。最重要的是,持续编译 JavaScript 和对 JavaScript 进行字节很有可能会失败。这将会产生两个不兼容的 JavaScript 语言,对于 web 有时非常不利。

此外,这样的字节码实际上对代码的大小和性能是否有帮助,还有待论证。

我们只是让解析器更快会怎样?

那岂不是很好,如果我们可以让解析器更快?不幸的是,虽然 JS 解析器有了很大改进,但改进的速度是在逐步放缓。

让我引用几个不能跳过或一直有效的步骤:

  • 处理外来编码,标记 Unicode 字节顺序和其他细节;
  • 找出 / 字符,是一个除法操作或者一个注释或正则表达式的开始;
  • 找出 ( 字符,是表达式的开始,一个函数调用的参数列表,箭头函数的参数列表等;
  • 找出这个字符串(分别是字符串模板、数组、函数等)在哪停止,这取决于所有模棱两可的问题;
  • 找出 let a 声明是否和其他的 let avar aconst a 声明冲突,实际上可能在稍后的代码出现;
  • 当遇到使用 eval 时,决定使用 4 个语义中的哪一个;
  • 确定哪些是真正的本地变量;
  • 等等

理想情况下,虚拟机开发者希望能够并行解析,或延迟直到我们知道我们实际上使用的语法在进行解析。事实上,最近的虚拟机实施这些战略。遗憾的是,JavaScript 语法中大量的标记含糊性大大增加了并发性的机会,同时必须抛出对语法错误的限制,从而限制了懒惰解析的机会。遗憾的是,JavaScript 语法中大量模棱两可的标记大大增加了并发性的机会,同时必须抛出对语法错误的限制,从而限制了懒惰解析的机会。

在任何情况下,虚拟机都需要进行昂贵的预分析步骤,可却往往适得其反,产生比常规的解析速度较慢,尤其是当应用在压缩编码时。

实际上,二进制 AST 建议旨在克服文本源 JavaScript 的语法和语义所带来的性能限制。

现在是什么情况?

我们发布这篇博客因为我们想让你 —— Web 开发人员或者工具开发商必须在尽可能早的了解二进制的 AST。到目前为止,我们从两组收集的反馈是非常好的,我们期待着与社区密切合作。

我们已经完成了一个早期的基准测试原型(因此不太实用),正在开发一个先进的原型,无论是对于工具还是 Firefox,但是我们还有几个月的时间去做一些有用的事情。

我会在几周内发布更多的细节。

阅读更多:


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOSReact前端后端产品设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏