mquickjs vs Node.js 对比
近期 quickjs 的作者发布了 mquickjs,在保持性能接近的情况下,内存占用大幅缩小,运行时整体内存占用只需要 100kb。
由于好奇 mquickjs 在内存和执行速度上到底怎样,我跑了一些跟 Node.js 和 QuickJS 的对比。
Node.js 作为目前最常用的运行时,在大而全的同时也有着一些自己的问题,比如启动时间长,资源占用多,V8 的 JIT 特性使得代码冷启动性能不好等等。这也使得社区出现了例如 bun、deno 等等多个运行时。严格来说 mquickjs 不是一个完整的 runtime,而是一个 engine,对标的应该是 V8。这里我们为了方便就直接跟 Node.js 来做对比了。
同时,与 QuickJS 的对比也可以看一下 mquickjs 有哪些特殊之处。
mquickjs 的使用场景主要在资源有限的嵌入式领域,或者终端设备的脚本执行等。由于自己做 SSR 的经历,本次更想要探索的是 Node.js 中这么一个场景:
- 有茫茫多的组件需要渲染:这意味着有非常多的 JS 文件,流量分布不均匀,存在大量的长尾流量。这些长尾流量在首次执行时性能不能太差,否则会导致渲染超时。
- 需要进行并行的渲染:使用线程或者进程并行渲染时,资源的占用情况会被放大,例如一个实例启动 100 个进程,内存占用就是 100 倍。
使用 Node.js 在这个场景中遇到了很多问题,那么 mquickjs 可以解决这个问题吗?之前我们尝试过 quickjs,这次看看 mquickjs 是否会有不同。
启动时间
- 测试文件:简单的
console.log('Hello, World!') - 测量工具:Unix
time命令
结果:
| 运行时 | 启动时间 |
|---|---|
| Node.js | 158ms |
| mquickjs | 2ms |
| QuickJS | 3ms |
可以看到 mquickjs 和 QuickJS 的启动速度几乎可以用”瞬时”来形容。可能非常适合临时拉起一个实例来做渲染,之后直接废弃。
当然,引擎的启动时间只是整个实例启动的一(小)部分,Node.js 中加载了很多 lib 库的逻辑,实际使用场景中还需要加载业务逻辑代码,这些都会占用很多时间。
SSR模拟测试(纯JS执行时间)
场景:模拟服务端渲染,1000次字符串拼接。
| 运行时 | 执行时间 |
|---|---|
| Node.js | 0.42ms |
| mquickjs | 9ms |
| QuickJS | 0.47ms |
我们可以看到 Node.js (V8) 和 QuickJS 在纯 JS 计算性能上要远远优于 mquickjs。
内存使用
| 场景 | Node.js | mquickjs | QuickJS |
|---|---|---|---|
| 简单脚本 | 46,184 KB | 1,664 KB | 2,688 KB |
| SSR模拟 | 46,700 KB | 10,880 KB | 2,816 KB |
| 加载1MB文件 | 48,052 KB | 5,452 KB | 3,960 KB |
可以看到 mquickjs 的内存占用确实非常少,仅为 Node.js 的 4-23%,这使得我们可以在实例中轻松部署非常多的 mquickjs 实例。一百个实例,光运行时的内存占用就是 4.5G -> 160M 的差距。
但我们也可以发现,JavaScript 代码的体积是不可避免的,如果代码量非常大(我之前的业务场景),优化代码的体积也是有价值的。另外也要注意 Node.js 里除了 V8 还有很多其他 lib 代码,这些代码也是要占体积的,而如果我们想要 mquickjs 跑起来,必然也要补充这些 lib 代码,内存差异会被一定程度抹平。
最后,虽然 mquickjs 初始化内存占用低,但加载逻辑代码后,内存占用会显著上升,高于 QuickJS,可能跟 mquickjs 的垃圾回收等内存管理机制有关系。
JIT 效果
V8 很快,很重要的一个原因就是可以在执行多次后进行优化,直接使用 native 代码来跑。我运行了 10 次 Mandelbrot 分形计算,观察编译器的优化效果。
Node.js(有JIT优化)
| 迭代次数 | 执行时间 | 性能变化 |
|---|---|---|
| 第0次 | 217ms | 冷启动(基线) |
| 第1-9次(均) | ~28ms | 性能提升约 87% |
首次运行较慢(217ms),后续运行稳定在 28ms 左右,性能提升了 **87%**。这是JIT编译器在运行时优化字节码的典型表现。
mquickjs(无JIT,解释执行)
| 迭代次数 | mquickjs 执行时间 (均) | QuickJS 执行时间 (均) | 性能变化 |
|---|---|---|---|
| 第0-9次 | ~79ms | ~67ms | 无变化 |
所有迭代执行时间一致,无任何优化。解释执行的性能稳定但较慢。
可以看到:
- mquickjs 和 QuickJS 性能一致性强,可预测性好,但性能绝对值与优化后的 V8 差距巨大。
- mquickjs 确实做到了与 QuickJS 接近的性能(单这个场景)。
微基准测试综合对比
mquickjs 的代码中有一个 microbench 测试用例,简单的修改就可以在 Node.js 和 QuickJS 中跑起来,结果如下_(总分根据各项测试的“纳秒/操作”计算得出)_:
| 指标 (越低越好) | Node.js (V8) | mquickjs | QuickJS |
|---|---|---|---|
| 总分 (相对性能) | 0.13x | 0.99x | 1.0x |
Node.js (V8) 在这个微基准测试中展现了压倒性的优势,其性能大约是 QuickJS 的 7-8倍,是 mquickjs 的 7.6倍。mquickjs 和 QuickJS 的性能则非常接近。
总结
可以看到 mquickjs 的内存占用确实非常小,但性能上跟 V8 差距还是很大的。
另外这些都是一些局部的测试,具体的选型还是需要看实际的使用场景。
对比过程中使用的脚本可以在 GitHub仓库中找到:javascript_runtimes。