如何加快 Node.js 应用的启动速度( 二 )


看来,require 是我们可以优化的第一个点 。
如何更快从上面得知,主要影响我们启动速度的是两个点,文件 I/O 和代码编译 。我们分别来看如何优化 。
? 文件 I/O
整个加载过程中,能够产生文件 I/O 的有两个操作:
一、查找模块
因为 Node.js 的模块查找其实是一个嗅探文件在指定目录列表里是否存在的过程,这其中会因为判断文件存不存在,产生大量的 Open 操作,在模块依赖比较复杂的场景,这个开销会比较大 。
二、读取模块内容
找到模块后,需要读取其中的内容,然后进入之后的编译过程,如果文件内容比较多,这个过程也会比较慢 。
那么,如何能够减少这些操作呢?既然模块依赖会产生很多 I/O 操作,那把模块扁平化,像前端代码一样,变成一个文件,是否可以加快速度呢?
说干就干,我们找到了社区中一个比较好的工具 ncc,我们把 serverless-runtime 这个模块打包一次,看看效果 。
服务器环境:
ncc build node_modules/serverless-runtime/src/index.tsnode require.js// 平均加载时间 934ms看起来效果不错,大概提升了 34% 左右的速度 。
但是,ncc 就没有问题嘛?我们写了如下的函数:
import * as _ from 'lodash';import * as Sequelize from 'sequelize';import * as Pandorajs from 'pandora';console.log('lodash: ', _);console.log('Sequelize: ', Sequelize);console.log('Pandorajs: ', Pandorajs);测试了启用 ncc 前后的差异:

如何加快 Node.js 应用的启动速度

文章插图
 
可以看到,ncc 之后启动时间反而变大了 。这种情况,是因为太多的模块打包到一个文件中,导致文件体积变大,整体加载时间延长 。可见,在使用 ncc 时,我们还需要考虑 tree-shaking 的问题 。
? 代码编译
我们可以看到,除了文件 I/O 外,另一个耗时的操作就是把 Javascript 代码编译成 v8 的字节码用来执行 。我们的很多模块,是公用的,并不是动态变化的,那么为什么每次都要编译呢?能不能编译好了之后,以后直接使用呢?
这个问题,V8 在 2015 年已经替我们想到了,在 Node.js v5.7.0 版本中,这个能力通过 VM.Script 的 cachedData暴露了出来 。而且,这些 cache 是跟 V8 版本相关的,所以一次编译,可以在多次分发 。
我们先来看下效果:
//使用 v8-compile-cache 在本地获得 cache,然后部署到服务器上node require.js// 平均耗时 868ms大概有 40% 的速度提升,看起来是一个不错的工具 。
但它也不够完美,在加载 code cache 后,所有的模块加载不需要编译,但是还是会有模块查找所产生的文件 I/O 操作 。
? 黑科技
如果我们把 require 函数做下修改,因为我们在函数加载过程中,所有的模块都是已知已经 cache 过的,那么我们可以直接通过 cache 文件加载模块,不用在查找模块是否存在,就可以通过一次文件 I/O 完成所有的模块加载,看起来是很理想的 。
不过,可能对远程调试等场景不够优化,源码索引上会有问题 。这个,之后会做进一步尝试 。
近期计划
有了上面的一些理论验证,我们准备在生产环境中将上述优化点,如:ncc、code cache,甚至 require 的黑科技,付诸实践,探索在加载速度,用户体验上的平衡点,以取得速度上的提升 。
其次,会 review 整个函数运行时的设计及业务逻辑,减少因为逻辑不合理导致的耗时,合理的业务逻辑,才能保证业务的高效运行 。
最后,Node.js 12 版本对内部的模块默认做了 code cache,对 Node.js 默认进程的启动速度提升比较明显,在服务器环境中,可以控制在 120ms 左右,也可以考虑引用尝试下 。
未来思考其实,V8 本身还提供了像 Snapshot 这样的能力,来加快本身的加载速度,这个方案在 Node.js 桌面开发中已经有所实践,比如 NW.js、Electron 等,一方面能够保护源码不泄露,一方面还能加快进程启动速度 。Node.js 12.6 的版本,也开启了 Node.js 进程本身的在 user code 加载前的 Snapshot 能力,但目前看起来启动速度提升不是很理想,在 10% ~ 15% 左右 。我们可以尝试将函数运行时以 Snapshot 的形式打包到 Node.js 中交付,不过效果我们暂时还没有定论,现阶段先着手于比较容易取得成果的方案,硬骨头后面在啃 。
另外,Java 的函数计算在考虑使用 GraalVM 这样方案,来加快启动速度,可以做到 10ms 级,不过会失去一些语言上的特性 。这个也是我们后续的一个研究方向,将函数运行时整体编译成 LLVM IR,最终转换成 native 代码运行 。不过又是另一块难啃的骨头 。


推荐阅读