返回博客首页
← 所有文章

使用 V8 代码缓存来最小化 Android 应用加载时间

2015 年 12 月 3 日 — 作者:Georgi Atanasov

NativeScript 框架存在的三个原因是:原生 UX、性能以及更简洁、更易于使用的跨平台移动应用编程模型。

为了在这三个方面取得卓越成就,过去两年里,NativeScript 框架内部投入了大量的工作,并且我们可以说,我们对框架的当前状态已经非常满意。在过去的几个月里,我们想要改进的一件事是 Android 设备上的加载时间。我们花了大量的时间来分析和优化客户端以及 Telerik 移动应用,并且成功地将 Android 上的加载时间缩短了 4 倍!

在这篇博文中,我将更详细地介绍我们用来实现这一出色成果的技术之一。 我们知道 Google 的 V8 JavaScript 引擎是一个优秀的虚拟机,工作完美且易于编程。但我们不知道的是,它实际上是一个强大的工具——让我告诉你为什么。

想法

最近我偶然发现了一篇博文,其中描述了 V8 作者称为“代码缓存”的技术。简而言之,V8 可以重用已编译的 JavaScript 代码来显著提高性能。换句话说,我们可以保留第一次 JavaScript 处理的结果(是的,V8 在编译后提供了所有必要的数据),将其保存到文件中,并在以后的应用运行中直接从该文件加载代码。

我对此功能进行了进一步的研究,并找到了另一篇有趣的文章,作者在其中分享了这种代码缓存带来的性能改进。正如作者的比较所示,我们谈论的是多倍的改进。

Code_Caching_Hashseed

当然,我渴望进行实验并将此功能集成到我们的 Android 运行时中。它很容易实现(是的,V8 易于编程),我针对我们内部的一个 Reddit 阅读器应用进行了实验。如你所知,NativeScript 的跨平台模块已经包含 900 KB 的纯 JavaScript 代码,因此缓存可能会带来巨大的改进。

结果

Script_Compile_Lazy

正如预期的那样,为每个脚本将编译结果保存到文件中会在第一次运行时增加一些开销,但在后续运行中会得到回报。令我惊讶的是,虽然效果显著,但并没有感觉到明显的改进,主要是因为总的编译时间本来就很短。

延迟或不延迟

我进一步研究了此功能的其他实现,只是为了确保我没有做错什么。在关于 Node.js 脚本加载性能的 GitHub 问题中,有人提到了延迟编译。

V8 足够智能,可以等到函数第一次使用时才编译它们。但这意味着它需要在应用生命周期的后期花费额外的时间来进行编译。此外,代码缓存对延迟编译的代码没有增值作用。这解释了为什么 NativeScript 模块编译不需要太多时间——因为某些代码部分最初没有被处理。

幸运的是,V8 具有“--nolazy”启动标志,可以禁用延迟编译功能。为了进行测量,我启用了它。这样我就可以更准确地测量编译花费的总时间。

Script_Compile_NoLazy

现在结果更符合我的预期。虽然我们在第一次运行时“牺牲”了大约 150 毫秒,但在后续执行中获得了大约 10 倍的改进。如果我们将结果与之前的图表进行比较,我们会发现当启用“--nolazy”时,编译需要更多时间。事实上,这段额外的时间,虽然在测量时没有出现,但会在以后的应用生命周期中遇到——例如,在加载用户界面时,因为 V8 需要处理尚未编译的函数。

试一试

此功能已随 NativeScript v 1.5.0 版本发布,但默认情况下未启用。我们决定让你选择是否接受应用第一次运行速度较慢以换取大幅改进的后续运行。你需要做的就是修改应用的 package.json(app/package.json)文件,如下所示:

"android": {
    "v8Flags" : "--nolazy --expose_gc",
    "codeCache" : "true"
}

 

你也可以尝试使用“--nolazy”标志,看看哪种方法最适合你。请与我们分享你的反馈,说明这是否应该成为默认行为——我们应该将其禁用还是应该默认启用它?

关于另一个 V8 标志“--expose_gc”的简要说明,它目前由 NativeScript 模块需要,并允许直接调用 V8 的垃圾回收器。因为我们正在同步两个垃圾回收器(JavaScript 和 Java),所以有时需要手动提示 JS 端对 Android 端的压力。

接下来是什么

我们一直在寻找改进 NativeScript 性能的方法。例如,在改进 V8 的性能中,作者讨论了另一个非常有趣的 V8 功能——自定义启动快照,它也有可能进一步缩短 Android 运行时的加载时间。我们甚至在这方面取得了一些进展,但我需要留到以后的文章中再讲。敬请期待 :)