Bun 如何将 postMessage(string) 提速 500 倍,远超 NodeJs 🚀🚀🚀

569 阅读4分钟

在 JavaScript 中,postMessage 是在多个工作线程之间传递数据的常用方法。在 Bun v1.2.21 中,postMessage(string) 的性能几乎与字符串大小无关。这对于多线程 JavaScript 服务器和命令行工具是一个巨大的改进。

20250902090321

通过避免对已知安全共享的字符串进行序列化,性能提高了多达 500 倍,并且在此基准测试中使用的峰值内存减少了约 22 倍。

字符串大小Bun 1.2.21Bun 1.2.20Node 24.6.0
11 字符543 ns598 ns806 ns
14 KB460 ns1,350 ns1,220 ns
3 MB593 ns326,290 ns242,110 ns

这种优化在你将字符串发送到工作线程时自动生效:

const response = await fetch("https://api.example.com/data");
const json = await response.text();

postMessage(json); // 对于大字符串,现在速度提升了 500 倍

这对于在工作线程之间传递大型 JSON 数据的应用程序特别有用,例如 API 服务器、数据处理管道和实时应用程序。

如下代码所示:

async function measureTime() {
  const url = "https://microsoftedge.github.io/Demos/json-dummy-data/5MB.json";

  // 开始计时
  console.time("Total Time");

  try {
    const response = await fetch(url);

    if (!response.ok) {
      throw new Error(`HTTP Error! Status: ${response.status}`);
    }

    // 等待JSON解析完成,但不将其赋值给变量,因为我们不需要使用它
    await response.json();

    console.timeEnd("Total Time");
  } catch (error) {
    // 如果发生错误,则结束计时并打印错误信息
    console.timeEnd("Total Time");
    console.error("An error occurred:", error);
  }
}

measureTime();

首先我们分别使用 NodeJs 和 Bun 的旧版本分别测试一下,最终输出结果如下图所示:

20250902091319

当我们把版本升级到最新之后,速度明显增高:

20250902091413

技术揭秘:JavaScriptCore 的优化

postMessage 通常使用结构化克隆算法(Structured Clone Algorithm)在发送到另一个线程之前序列化数据。这意味着将字符串的每个字节复制到一个新缓冲区,然后在另一端反序列化。

但问题是:在 JavaScriptCore(Bun 使用的引擎)中,字符串已经是线程安全的引用计数对象。字符串数据在创建后是不可变的,引用计数使用 std::atomic

class StringImplShape {
    std::atomic<unsigned> m_refCount;  // 线程安全!
    unsigned m_length;                  // 不可变
    union {
        const LChar* m_data8;           // 不可变
        const char16_t* m_data16;       // 不可变
    };
    mutable unsigned m_hashAndFlags;    // 唯一可变部分
};

因此,如果字符串已经是线程安全的,为什么在同一进程中的线程之间发送时还要进行序列化呢?([bun.com][1])

寻找快速路径

并非所有字符串都可以安全地共享。我们识别出三种需要序列化的类型:

  1. 原子字符串(Atom strings):线程本地的属性名和符号。

  2. 子字符串(Substrings):指向其他具有复杂生命周期的字符串。

  3. 绳状字符串(Rope strings):通过操作如 "foo" + "bar".slice() 创建的字符串。

对于其他所有字符串,我们可以完全跳过序列化。我们只需要确保在共享之前计算出惰性计算的哈希值(因为这是唯一可变的部分):

WTF::String toCrossThreadShareable(WTF::String& string)
{
    auto* impl = string.impl();

    // 不能共享原子、符号或子字符串
    if (impl->isAtom() || impl->isSymbol() ||
        impl->bufferOwnership() == StringImpl::BufferSubstring)
        return string.isolatedCopy();

    // 在共享之前强制计算哈希
    impl->hash();

    // 防止该线程进行原子化。
    impl->setNeverAtomicize();

    return string;  // 直接共享指针!
}

toCrossThreadShareable 函数通过识别哪些字符串可以安全地共享而无需序列化,实现了性能的显著提升。这种优化在多线程 JavaScript 服务器和命令行工具中尤为重要。理解其背后的原理有助于我们在开发中更高效地处理字符串数据。

快速路径条件

当满足以下条件时,优化将生效:

  • 你正在使用 postMessagestructuredClone

  • 你只发送一个字符串(而不是混合数据)。

  • 字符串不是子字符串、绳状字符串、原子或符号。

  • 你将字符串发送到同一进程中的另一个线程。

  • 字符串长度 ≥ 256 字符。

这涵盖了在工作线程之间发送字符串的极其常见的模式。

总结

通过识别哪些字符串可以安全地共享而无需序列化,Bun 在 postMessage(string) 上实现了显著的性能提升。这种优化对于多线程 JavaScript 服务器和命令行工具尤其有用。