译使用BinaryAST加快JavaScript脚本的解析速度

43次阅读

共计 3759 个字符,预计需要花费 10 分钟才能阅读完成。

原文:Faster script loading with BinaryAST?

本文首发于公众号:符合预期的 CoyPan

JavaScirpt 的冷启动

web 应用的表现,越来越受制于启动时间。我们已经习惯于使用大量的 JavaScript 代码来开发丰富的 web 交互体验。从 HTTPArchive 上,我们可以看到,一个移动设备平均会加载 350KB 的 JavaSript 代码,10% 的页面会加载超过 1MB 的 JavaScipt 代码。复杂的交互会使得这个数字越来越高。

尽管有缓存的帮助,但是常见的站点都会频繁的发布新代码,导致冷启动 (首次加载) 时间十分的重要。随着浏览器将缓存按照域来划分以防止跨站点泄露,冷启动的重要性正在增加,即使是从 CDN 加载的常用资源来说也是如此,因为它们不再能够安全地共享。

通常情况下,当我们谈论冷启动性能时,最常见的因素就是下载速度。然后,在现在的富交互页面上,另外一个影响冷启动的很重要因素是:JavaScipt 的解析时间。咋看起来会有点让人意外,但是是合理的:在开始执行代码前,引擎不得不先解析下载的 JavaScript,确保脚本没有语法错误,然后将其编译为基本的字节码。随着网络变得越来越快,JavaScipt 的解析和编译可能会成为影响冷启动的最主要因素。

设备能力(CPU 或内存性能)是影响 JavaScript 解析时间和相应应用程序启动时间变化的最重要因素。在现代桌面或高端移动设备上,一个 1MB 的 javascript 文件需要 100 毫秒的解析时间,但在普通手机上,解析时间可以超过一秒钟。

关于在不同设备上 javascript 解析、编译和执行的总体成本,这篇文章给出了详细的介绍。以 news.google.com 为例,在 Pixel 2 上,解析、编译、执行 JS 的总耗时为 4s,而在一些低端的设备上,需要 28s。

虽然引擎不断提高原始解析性能,尤其是在过去的一年里,V8 引擎的性能翻了一番,并且使更多的东西脱离了主线程,但解析器仍然需要做大量可能不必要的工作,这些工作会消耗内存、电池,并可能延迟有用资源的处理。

BinaryAST 提案

“BinaryAST” 应运而生。BinaryAST 是 Mozilla 提出并积极开发的一种新的在线 javascript 格式,旨在加快解析速度,同时保持原始 javascript 的语义不变。它的实现方式是:使用有效的二进制来表示代码和数据结构,并且存储和提供额外的信息来提前指导解析器工作。

之所以使用 BinaryAST 这个名字,是因为这种格式以 AST 的方式存储 JavaScript 源码,然后编码到一个二进制文件中。该规范位于 tc39.github.io/proposal-binary-ast,目前正由 Mozilla、Facebook、Bloomberg 和 CloudFlare 的工程师开发。

解析 JavaScript

对于要在浏览器中执行的常规 JavaScript 代码,源代码被解析为一个称为 AST 的中间表示,它描述了代码的语法结构。然后,可以将此 AST 编译为字节代码或本机代码以供执行。

一段简单的将两个数相加的代码,用 AST 表示为:

解析 JavaScript 不是一项简单的任务;无论使用哪种优化,它仍然需要逐字符读取整个文本文件,同时跟踪额外的上下文进行语法分析。

BinaryAST 的目标是通过在解析器需要的时间和地点提供额外的信息和上下文,来降低复杂性和浏览器解析器必须完成的总体工作量。

要执行以 BinaryAST 方式传递的 JavaScript,所需要的唯一步骤是:

BinaryAST 的另一个好处是它可以只解析启动所需的关键代码,完全跳过未使用的位。这可以显著提高初始加载时间。

这篇文章将更加详细地描述解析 JavaScipt 时遇到的挑战,解释我们是如何克服这些问题的,以及我们是如何在 Worker 中运行代码解释器的。

提升

JavaScript 依赖于提升所有声明——变量、函数、类。提升是语言的一个属性,它允许你在语法上使用之后,再去声明变量,函数,类等。

让我们来看下面这个例子:

function f() {return g();
}

function g() {return 42;}

在这里,当解析器查看 F 的主体时,它还不知道 G 指的是什么——它可能是一个已经存在的全局函数或者在同一个文件中进一步声明的某个函数——所以它无法最终解析原始函数并开始实际编译。

BinaryAST 通过存储所有作用域信息并使其在实际表达式之前可用来解决这个问题。

用 JSON 表示初始的 AST 和增强的 AST 之前的区别,如下图所示:

延迟解析

现代引擎用来改进解析时间的一种常见技术是延迟解析。它利用了这样一个事实:许多网站包含的 javascript 比实际需要的要多,特别是对于新的网站。

例如,从文本中解析数字、布尔值甚至字符串等低级类型需要额外的分析和计算。这是没有必要的。您可以首先将它们存储和读取为本机二进制编码值,然后直接在另一端读取。

另一个问题是语法本身的歧义。这在 ES5 世界中已经是一个问题,但通常可以通过一些基于以前看到的标记的额外记录来解决。然而,在 ES6+ 中,有些东西可能一直模糊不清,直到它们被完全解析为止。

例如,一个标记序列如下:

(a, {b: c, d}, [e = 1])...

上述标记序列可以是一个用嵌套的对象和数组文本以及赋值来启动带括号的逗号表达式:

(a, {b: c, d}, [e = 1]); // 这是一个表达式

也可以是一个带有嵌套对象和数组模式的箭头表达式函数的参数列表和默认值:

(a, {b: c, d}, [e = 1]) => … // 这是一个参数列表

这两种表示都是完全有效的,但语义完全不同,在看到最后一个标记之前,你无法知道要处理的是哪个。

为了解决这一问题,解析器通常要么回溯,这很容易以指数级的速度变慢,要么将内容解析为能够同时保存表达式和模式的中间节点类型,并进行后续的转换。后一种方法保留了线性性能,但使实现更加复杂,需要保留更多的状态。

在 BinaryAST 格式下,这个问题不再存在。因为解析器在开始解析内容前就可以看到每个节点的类型。

展示实验数据

请记住,该提案处于非常早期的阶段,当前的基准和演示不能代表最终结果。

如前所述,BinaryAST 可以标记应该提前进行惰性分析的函数。通过在编码器 https://github.com/binast/binjs-ref/blob/b72aff7dac7c692a604e91f166028af957cdcda5/crates/binjs_es6/src/lazy.rs#L43 中使用不同级别的惰性化,对一些流行的 javascript 库运行测试时,我们发现了以下速度的提升。

Level 0 (no functions are lazified)

在两个解析器中都禁用了惰性解析之后,原始解析速度提高了 3% 到 10%。

Level 3 (functions up to 3 levels deep are lazified)

但是,通过设置为跳过最多嵌套 3 层的函数函数,我们可以看到解析时间在 90% 到 97% 之间的显著改进。正如本文前面提到的,BinaryAST 通过完全跳过标记的函数,使延迟解析基本上是无开销的。

通过下面的包含 1.2MB JavaScript 的示例程序 https://github.com/cloudflare…
https://serve-binjs.that-test…
我们得到了以下的初始脚本执行数据:

以下是一段视频,它将让您了解移动 FireFox 用户所看到的改进(在本例中,显示整个页面启动时间):

下一步是开始在现实网站上收集数据,同时改进底层格式。

如何在我的站点上测试 BinaryAST?

我们已经开源了 Worker 的源代码,以便将其安装到任何 CloudFlare 区域:

https://github.com/binast/bin…

目前需要注意的一件事是,即使结果存储在缓存中,初始编码仍然是一个昂贵的过程,并且可能很容易达到任何重要的 javascript 文件的 CPU 限制,并返回到未编码的变量。我们正在努力改善这种情况,在接下来的日子里,将 BinaryAST 编码器作为一个单独的功能发布,并有更宽松的限制。

同时,如果你想在更大的脚本上使用 BinaryAST,另一种选择是使用 https://github.com/binast/bin…,提前对 javascript 文件进行预编码。然后,在浏览器支持和请求时,可以使用 https://github.com/cloudflare…,来处理生成的 BinaryAST 文件。

在客户端,您当前需要下载 Firefox Nightly,转到 about:config 并通过以下选项启用无限制的 binaryast 支持。

现在,当打开一个安装了 Worker 的网站时,Firefox 会自动得到 BinaryAST 而不是 javascript。

总结

现代应用程序中的 javascript 数量正在给所有消费者带来性能挑战。引擎供应商正在尝试各种不同的方法来改善这种情况——一些侧重于原始解码性能,一些侧重于并行操作以减少总体延迟,一些致力于研究用于数据表示的新的优化格式,还有一些正在发明和改进用于网络交付的协议。

不管是哪一个,我们都有一个共同的目标,那就是让网络变得更好、更快。


正文完
 0