2021 年 JavaScript Promise 性能对比
我们正生活在一个「Any application that can be written in JavaScript, will eventually be written in JavaScript」的时代。作为一门兼具动态性和简单性的语言,JavaScript 已经占领了客户端、服务端,甚至在机器学习中也占据一席之地;不可避免的,异步执行也逐渐成为这门语言不可缺少的一部分。
TL; DR
- Bluebird 依然是速度最快、内存占用最少的 Promise 实现
- Runtime 的 async / await 实现越来越快、顺序执行的性能已经超过 Native Promise,占用的内存也更少
- 对于平行并发执行的 Promise,Bluebird 的性能依然一骑绝尘。编写运行在 Node.js 上的服务端程序仍然需要评估是否有必要引入 Bluebird
- 所有对 Async / Await 的转译都不可避免的引入性能损耗;TypeScript Compiler(tsc)转译时引入的性能开销尤为明显,一般比原生 Async / Await 要慢至少两倍,同时要消耗更多的内存。
背景知识
Node.js / v8 的 Promise 实现
关于 Bluebird vs Native,相信大部分读者肯定有一个问题:Bluebird 作为 Promise 的一个 JavaScript 实现,竟然会比 V8(Node.js 是基于 Chrome 的 V8 JavaScript 引擎的 Runtime)的 Native Promise 实现还快?
实际上在 2017 年之前,V8 的 Promise 也是用 JavaScript 实现的、且并不完美,例如 在 Promise 初始化时就分配数组给 Promise Handler 导致不必要的内存占用;V8 直到 2016 年 5 月才对此进行了优化(V8 5.3.55)。V8 到 2016 年 12 月开始使用 C++ 实现 Promise(V8 5.7.142)、在 Node.js 8 中落地(Node.js 7 使用的是 V8 5.5,Node.js 8 使用的是 V8 5.8)。
衡量 Promise 性能的方式
Gorgi Kosev 在 2013 年 8 月发布了「Analysis of generators and other async patterns in node」,详细介绍了 Generator Function,并与当时常见的异步实现(如 Q.js
)、回调地狱的解决方案(flatten.js
)的性能和编写难度进行了比较。Gorgi Kosev 提供了一段基于 Doxbee 的业务伪代码、涉及「数据库连接」「数据库事务回滚」「文件上传」「查询执行」等典型的 CRUD 和阻塞操作。后来,Bluebird 的作者为这段伪代码补充了一个 mock context,「Doxbee Benchmark」便成为了衡量 JavaScript 异步实现的性能的标准方法。V8 团队的 Maya Lekova 在 修改 ECMAScript Spec 时,也使用了 Doxbee Benchmark 的数据来阐述修改的必要性。
顺便一提,早期 Promise 实现的性能完全无法入眼、一直被 JavaScript 开发者诟病,直到 2013 年 12 月 Petka Antonov 发布了 Bluebird 的首个版本,JavaScript 社区对 Promise 的印象才大幅改观。
Bluebird 为什么这么快?
Bluebird 发布时,比同类实现快了将近 100 倍、内存占用却不到同类的十分之一;数年过去了,JavaScript 引擎的 JIT 不断进化(例如 V8 用 Turbofan 代替了 CrankShift),Bluebird 的性能依然在众多实现中出类拔萃脱颖而出。2016 年 Bluebird 的作者 Petka Antonov 写过一篇文章「Three JavaScript performance fundamentals that make Bluebird fast」,分享了三个简单且行之有效的 JavaScript 性能优化技巧。
Benchmark
此次 Benchmark 基于 V8 团队衡量 Async 优化、修改 ES Spec 时使用的 v8/promise-performance-tests Benchmark Suite,额外增加了内存 RSS 统计,你可以前往 查看 Fork 后修改的版本。
运行环境为:
OS: Darwin 21.1.0 x64
CPU: Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz x 16
Memory: 32768 MiB
Bluebird vs Native Promise vs Native Async / Await
顺序执行
顺序执行的 Promise 的特点是后一个 Promise 会用到前一个 Promise resolve 的值、只能在前一个 Promise fullfil 后执行:
const user = await fetch('/api/users/1');
const job = await fetch(`/api/jobs/${user.jobId}`);
const colleagues = await fetch(`/api/users/job/${job.id}`);
从 Node.js 12 开始,async/await 异步顺序执行的速度最快、占用内存最少,和 Node.js 12 使用的 V8 版本包含 Fast Async 的 Patch 不无关系;同时,Bluebird 比 Native Promise 的速度要快,占用的内存也更少。
平行执行
平行执行的 Promise 特点是数个 Promise 之间不存在依赖关系;虽然 JavaScript 是单线程的,当一个 Promise(非阻塞地)从外部 Worker(如 Network、File I/O 等)等待响应数据时,Runtime 可以将下一个 Promise 塞入 Event Loop 中:
const userIds = [21, 42, 84, 168];
const users = await Promise.all(userIds.map(id => fetch(`/api/users/${id}`)));
平行执行的 Promise 的特点是使用 Promise.all
或 Promise.allSettled
;Bluebird 除 Bluebird.all
以外,还有 Bluebird.map
和 Bluebird.join
可被用于平行执行。
Bluebird 在平行执行时的性能一骑绝尘,比 Native 实现速度快 2~3 倍、内存占用却微不足道。
Native Promise vs JavaScript Promise
截至本文写就,绝大部分浏览器均已支持 Promise。但是如果要为古董浏览器如 IE 提供 Promise 支持,则依然需要使用 JavaScript 实现的 Polyfill。
参与 Benchmark 的 Promise 实现有:
- Bluebird
- core-js(Babel、swc 都依赖的 polyfill 集合)
- es6-promise@npm
- es6-promise-polyfill@npm
- promise@npm
- promise-polyfill@npm
- Q.js(支持
new Q.Promise
和Q.Promise
两种使用方式,测试时调用不使用new
) - SPromiseMeSpeed (自称是最快的 Promise 实现、比 Bluebird 还快 2~548 倍)
- Zousan
顺序执行
不出意外,Bluebird 顺序执行的性能比 Native 还要优秀,内存占用更是不到 Native 的 1/3;core-js
、SPromiseMeSpeed
、promise@npm
和 es6-promise-polyfill@npm
的性能与内存占用和 Native 实现接近。
平行执行
Bluebird 在平行执行上的表现依然一骑绝尘,promise@npm
也取得了类似的不凡成绩;而 core-js
等提供 Polyfill 则显得些许力不从心。
Async / Await
截止到本文写就,不支持 Async Function 的浏览器也已经屈指可数。如果要向下兼容仅支持 ES2016 甚至 ES5 的浏览器的话,依然需要通过转译的方式来模拟 Async Function 的行为。
参与 Benchmark 的转译器有:
- Babel 默认使用的
regenerator-runtime
- Babel 的一个非官方插件 fast-async
- Babel 的另一个非官方插件 babel-plugin-transform-async-to-promises
- tsc,TypeScript 的官方 Compiler
Benchmark 包括顺序执行(doxbee)、平行执行(parallel)和一个由 v8 提供的 Fibonacci 的计算测试:
async function* fibonacciSequence() {
for (let a = 0, b = 1; ;) {
yield a;
const c = a + b;
a = b;
b = c;
}
}
async function fibonacci(id, n) {
for await (const value of fibonacciSequence()) {
if (n-- === 0) return value;
}
};