加入收藏 | 设为首页 | 会员中心 | 我要投稿 核心网 (https://www.hxwgxz.com/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 建站 > 正文

V8是如何快速地解析JavaScript延迟解析

发布时间:2019-05-29 11:55:20 所属栏目:建站 来源:Web前端程序员
导读:解析是将源代码转换成一个中间表示形式供编译器使用的步骤(在V8中,是字节码编译器Ignition)。解析和编译发生在web页面启动的关键路径上,在启动期间,并不是所有提供给浏览器的函数都需要被调用。尽管开发人员可以使用异步和延迟脚本来延迟这些代码的加载

实际上,忽略变量声明和顶层函数的引用是不正确的。ECMAScript规范要求在第一次解析脚本时要检测各种类型的变量冲突。例如,如果一个变量在同一作用域内被两次声明为词法变量,则被认为是early SyntaxError。因为我们的预解析器只是跳过了变量声明,所以在预解析过程中它将允许代码错误地运行。此时我们认为性能上的胜利使对规范的违反情有可原。现在预解析器 能正确地跟踪变量,尽管如此,我们还是应该在没有明显性能代价的情况下消除这类与变量解析相关的违反规范的行为。

跳过内部函数

如前所述,当第一次调用一个预解析的函数时,我们将对其进行完全解析,并将生成的AST编译为字节码。

V8是如何快速地解析JavaScript: 延迟解析

该函数直接指向外部上下文,其中包含内部函数需要使用的变量声明的值。为了允许函数的延迟编译(并支持调试器),上下文会指向一个名为ScopeInfo的元数据对象。ScopeInfo对象描述了上下文中列出的变量。这意味着在编译内部函数时,我们可以计算变量在上下文链中的位置。

但是,要计算延迟编译的函数本身是否需要上下文,我们需要再次执行范围解析: 我们需要知道嵌套在延迟编译的函数中的函数是否引用了由延迟函数声明的变量。我们可以通过重新解析这些函数来计算出来。这正是V8在升级到V8v6.3/Chrome63之前所做的。但是,这并不是理想的性能最优的方法,因为它使资源大小和解析成本之间的关系变成非线性: 我们将尽可能多地解析嵌套函数。除了动态程序的自然嵌套之外,JavaScript打包器通常用“即时调用函数表达式”(IIFEs)的方式来包装代码,这使得大多数JavaScript程序具有多个嵌套层。

V8是如何快速地解析JavaScript: 延迟解析

每次重新解析至少会增加解析函数的成本。

为了避免非线性性能开销,我们甚至在预解析过程中执行全作用域解析。我们存储了足够的元数据,这样我们稍后就可以简单地跳过内部函数,而不必重新解析它们。一种方法是存储由内部函数引用的变量名。这样做的存储成本很高,并要求我们仍然进行重复工作:我们已经在预解析期间执行了变量解析。

相反,我们将在变量分配的地方将每一个变量序列化为它的一个密集标记数组。当我们延迟解析一个函数时,变量按照预解析器看到的顺序被重新创建,我们可以简单地将元数据应用于这些变量。现在函数已经编译完成,已经不再需要变量分配元数据了,这样它就可以被当做垃圾进行回收。由于我们只需要这个元数据来处理实际包含内部函数的函数,所以大部分函数甚至不需要这个元数据,从而显著地降低了内存开销。

V8是如何快速地解析JavaScript: 延迟解析

通过跟踪预解析的函数的元数据,我们可以完全跳过内部函数。

跳过内部函数的性能影响是非线性的,就像重新预解析内部函数的开销一样。有些站点将它们的所有函数都提升到了顶层范围。因为它们的嵌套层数总是0,所以开销也总是0。然而,许多现代的站点实际上都有许多深层嵌套函数。当V8 v6.3 / Chrome 63启动该特性时,我们就会在这些站点上看到显著的改进。启用该特性的主要优点是,现在代码的嵌套深度已经无关紧要: 任何函数最多只预解析一次,完全解析一次[1]。

V8是如何快速地解析JavaScript: 延迟解析

主线程和非主线程的解析时间,以及运行“跳过内部函数”前后都得到了优化。

随时调用函数表达式

如前所述,打包器通常通过将模块代码封装在一个它们即时调用的闭包中,来将多个模块组合到一个文件中。这为模块提供了隔离,允许它们像脚本中唯一的代码一样运行。这些函数本质上是嵌套的脚本;脚本执行时这些函数会立即被调用。打包器通常以带圆括号的函数,即 (function(){…})(),的形式提供即时调用函数表达式(IIFEs,发音为“iffies”)。

由于这些函数在脚本执行期间是立即需要的,所以预解析这些函数并不理想。在脚本的顶层执行过程中,我们急需这些函数被编译,所以我们会完全解析和编译这些函数。这意味着,我们在前期解析越快,代码运行时启动就越快,并且不会产生不必要的额外成本。

你可能会问,为什么不直接编译调用的函数呢?虽然开发人员在一个函数被调用时能很容易注意到它,但是对于解析器情况则不同。解析器在开始解析函数之前需要决定该函数是需要立即编译还是推迟编译。语法中存在的歧义使得简单地快速扫描到函数末尾变得很困难,而且成本很快就与常规预解析的成本一样。

因此V8有两个简单的模式,它可以将函数识别为随时调用函数表达式(PIFEs,发音为“piffies”),这样它会快速解析并编译一个函数:

如果一个函数是一个带圆括号的函数表达式,即(function(){…}),我们假设它将被调用。我们一看到这个模式的开始,即(function,就立即做出这个假设。

在V8 v5.7 / Chrome 57中我们也检测了由UglifyJS生成的模式!function(){…}(),function(){…}(),function(){…}()。一旦我们看到!function或者function后面如果紧跟着一个PIFE,那么这个检测就起作用了。

由于V8会立即编译PIFEs,所以它们可以被用作配置文件导向的反馈[2],通知浏览器启动需要哪些函数。

(编辑:核心网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

热点阅读