2021 年 JavaScript Promise 性能对比

2021 年 JavaScript Promise 性能对比

技术向约 3.4 千字

我们正生活在一个「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}`);
12

从 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.allPromise.allSettled;Bluebird 除 Bluebird.all 以外,还有 Bluebird.mapBluebird.join 可被用于平行执行。

34

Bluebird 在平行执行时的性能一骑绝尘,比 Native 实现速度快 2~3 倍、内存占用却微不足道。

Native Promise vs JavaScript Promise

截至本文写就,绝大部分浏览器均已支持 Promise。但是如果要为古董浏览器如 IE 提供 Promise 支持,则依然需要使用 JavaScript 实现的 Polyfill。

参与 Benchmark 的 Promise 实现有:

顺序执行

5

不出意外,Bluebird 顺序执行的性能比 Native 还要优秀,内存占用更是不到 Native 的 1/3;core-jsSPromiseMeSpeedpromise@npmes6-promise-polyfill@npm 的性能与内存占用和 Native 实现接近。

平行执行

6

Bluebird 在平行执行上的表现依然一骑绝尘,promise@npm 也取得了类似的不凡成绩;而 core-js 等提供 Polyfill 则显得些许力不从心。

Async / Await

截止到本文写就,不支持 Async Function 的浏览器也已经屈指可数。如果要向下兼容仅支持 ES2016 甚至 ES5 的浏览器的话,依然需要通过转译的方式来模拟 Async Function 的行为。

参与 Benchmark 的转译器有:

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;
  }
};
78
2021 年 JavaScript Promise 性能对比
本文作者
Sukka
发布于
2021-12-07
许可协议
转载或引用本文时请遵守许可协议,注明出处、不得用于商业用途!
本文最后更新于 201 天前,文中所描述的信息可能已发生改变
喜欢这篇文章?为什么不打赏一下作者呢?
爱发电
评论加载中 ...