不用灰心,你并不是唯一纠���于如何提升启动时间的人,我们 V8 团队也一直在努力。我们发现之前的某个评测工具 Octane 是个不错的对于真实场景的模拟,它在微型框架与冷启动方面很符合真实的用户习惯。而基于这些工具,V8 团队在过去的工作中也实现了大约 25% 的启动性能提升:
本部分我们就会对过去几年中我们使用的提升语法解析与编译时间的技巧进行阐述。
代码缓存Chrome 42 开始引入了所谓的 代码缓存 的概念,为我们提供了一种存放编译后的代码副本的机制,从而当用户二次访问该页面时可以避免脚本抓取、解析与编译这些步骤。除以之外,我们还发现在重复访问的时候这种机制还能避免 40% 左右的编译时间,这里我会深入介绍一些内容:
代码缓存会对于那些在 72 小时之内重复执行的脚本起作用。
对于 Service Worker 中的脚本,代码缓存同样对 72 小时之内的脚本起作用。
对于利用 Service Worker 缓存在 Cache Storage 中的脚本,代码缓存能在脚本首次执行的时候起作用。
总而言之,对于主动缓存的 JavaScript 代码,最多在第三次调用的时候其能够跳过语法分析与编译的步骤。我们可以通过 chrome://flags/#v8-cache-strategies-for-cache-storage 来查看其中的差异,也可以设置 js-flags=profile-deserialization 运行 Chrome 来查看代码是否加载自代码缓存。不过需要注意的是,代码缓存机制仅会缓存那些经过编译的代码,主要是指那些顶层的往往用于设置全局变量的代码。而对于类似于函数定义这样懒编译的代码并不会被缓存,不过 IIFE 同样被包含在了 V8 中,因此这些函数也是可以被缓存的。
Script StreamingScript Streaming 允许在后台线程中对异步脚本执行解析操作,可以对于页面加载时间有大概 10% 的提升。上文也提到过,这个机制同样会对同步脚本起作用。
这个特性倒是第一次提及,因此 V8 会允许所有的脚本,即使阻塞型的 <script src=''> 脚本也可以由后台线程进行解析。不过缺陷就是目前仅有一个 streaming 后台线程存在,因此我们建议首先解析大的、关键性的脚本。在实践中,我们建议将 <script defer> 添加到 <head> 块内,这样浏览器引擎就能够尽早地发现需要解析的脚本,然后将其分配给后台线程进行处理。我们也可以查看 DevTools Timeline 来确定脚本是否被后台解析,特别是当你存在某个关键性脚本需要解析的时候,更需要确定该脚本是由 streaming 线程解析的。
语法解析 & 编译优化我们同样致力于打造更轻量级、更快的解析器,目前 V8 主线程中最大的瓶颈在于所谓的非线性解析消耗。譬如我们有如下的代码片:
(function (global, module) { … })(this, function module() { my functions })V8 并不知道我们编译主脚本的时候是否需要 module 这个模块,因此我们会暂时放弃编译它。而当我们打算编译 module 时,我们需要重分析所有的内部函数。这也就是所谓的 V8 解析时间非线性的原因,任何一个处于 N 层深度的函数都有可能被重新分析 N 次。V8 已经能够在首次编译的时候搜集所有内部函数的信息,因此在未来的编译过程中 V8 会忽略所有的内部函数。对于上面这种 module 形式的函数会是很大的性能提升,V8 同样在寻找合适的分流机制以保证启动时能在后台线程中执行 JavaScript 编译过程。
预编译 JavaScript?