为什么利用 vm.Script 可以实现 Node.js 中的 code cache
vm.Script 可以实现 Node.js 中的 code cache,但是这还不够。
code cache 是 V8 中的一个特性,简单说就是 JavaScript 代码在执行前,取消进行解析和编译,才能正确执行,解析编译过程是耗时的,V8 暴露了一个方法,可以将编译产物序列化存储下来,下次再执行相同一段代码时,就可以用之前缓存的内容,节省了解析编译的时间。
code cache 在浏览器中可以在页面多次打开,或者多个 tab 中执行相同的代码时,节省 JavaScript 代码的编译时间。在 Node.js 中,也可以利用这一特性做一些优化,例如多个实例间共享code cache节省启动时间。又或者在启动时预先编译代码,节省第一次请求时的代码编译耗时。
Node.js 中有一个很好用的库可以直接使用code cache:v8-compile-cache,如果我们查看他的代码,会发现他是通过new vm.Script
来获取到编译产出的结果的。
1 | var script = new vm.Script(wrapper, { |
vm.Script 的实现原理
new vm.Script()
可以用来预编译代码,v8-compile-cache 里面就是用的这个。
vm.Script 定义在 Node.js 源码的 lib/vm.js 文件中。
这个类的构造函数本身没做什么内容,但是它继承了 ContextifyScript 这个类。vm.Script 的构造函数取了一下各种参数,然后就调用了 super。
1 | // Calling `ReThrow()` on a native TryCatch does not generate a new |
所以主要工作都是在 ContextifyScript
这个类中做的,ContextifyScript
这个类是 C++ 实现的,文件位置:
src/node_contextify.cc
直接看 ContextifyScript::New 的定义,这个函数前面主要处理了各种参数,之后调用了:
v8::ScriptCompiler::CompileUnboundScript
1 | MaybeLocal<UnboundScript> v8_script = ScriptCompiler::CompileUnboundScript( |
之后又调用了:v8::ScriptCompiler::CreateCodeCache
1 | ScriptCompiler::CreateCodeCache(v8_script.ToLocalChecked()) |
所以就是使用了 V8 的这两个 API。
v8::ScriptCompiler::CompileUnboundScript
https://v8.github.io/api/head/classv8_1_1ScriptCompiler.html#a2f7bbea025f3b6bc7c3a3a5c2f7421dc
查阅文档,说这个方法可以在不绑定 Context 的情况下编译出一个 Compiled script object。
同时编译产物中会有 Cached data,这个 Cached data 可以作为缓存,替代代码编译过程。
这就完了?
令人沮丧的是,使用code cache只能使得文件加载变快,但 Node.js 中更大的问题在于:相同函数前几次执行会比较慢,后面才会变快,例如:
执行这个 render 函数 100 次,第一次耗时在 10ms,第二次 5ms,第三次 2ms,后面就一直维持在 1ms了。
这会造成什么问题呢?线上每次 Node.js 服务重启后,前几次请求的耗时会非常长,很有可能造成超时拒绝。
实例数小的时候还好,实例数多了以后,每次重启都是大量的请求超时。
目前还没找到很好的解决办法。
别的语言是什么情况呢?
好奇试了一下 Go 执行起来会是什么样:
可以看到 Go 没有这个问题,但 Go 每次执行波动很大。Node.js 虽然有前几次请求慢的情况,但是多次执行以后就非常稳定了,每次都是一样耗时。