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

通过避免对已知安全共享的字符串进行序列化,性能提高了多达 500 倍,并且在此基准测试中使用的峰值内存减少了约 22 倍。
字符串大小 | Bun 1.2.21 | Bun 1.2.20 | Node 24.6.0 |
---|---|---|---|
11 字符 | 543 ns | 598 ns | 806 ns |
14 KB | 460 ns | 1,350 ns | 1,220 ns |
3 MB | 593 ns | 326,290 ns | 242,110 ns |
这种优化在你将字符串发送到工作线程时自动生效:
javascript
const response = await fetch("https://api.example.com/data");
const json = await response.text();
postMessage(json); // 对于大字符串,现在速度提升了 500 倍
这对于在工作线程之间传递大型 JSON 数据的应用程序特别有用,例如 API 服务器、数据处理管道和实时应用程序。
如下代码所示:
ts
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 的旧版本分别测试一下,最终输出结果如下图所示:

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

技术揭秘:JavaScriptCore 的优化
postMessage
通常使用结构化克隆算法(Structured Clone Algorithm
)在发送到另一个线程之前序列化数据。这意味着将字符串的每个字节复制到一个新缓冲区,然后在另一端反序列化。
但问题是:在 JavaScriptCore(Bun 使用的引擎)中,字符串已经是线程安全的引用计数对象。字符串数据在创建后是不可变的,引用计数使用 std::atomic
:
cpp
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])
寻找快速路径
并非所有字符串都可以安全地共享。我们识别出三种需要序列化的类型:
-
原子字符串(Atom strings):线程本地的属性名和符号。
-
子字符串(Substrings):指向其他具有复杂生命周期的字符串。
-
绳状字符串(Rope strings):通过操作如
"foo" + "bar"
或.slice()
创建的字符串。
对于其他所有字符串,我们可以完全跳过序列化。我们只需要确保在共享之前计算出惰性计算的哈希值(因为这是唯一可变的部分):
cpp
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 服务器和命令行工具中尤为重要。理解其背后的原理有助于我们在开发中更高效地处理字符串数据。
快速路径条件
当满足以下条件时,优化将生效:
-
你正在使用
postMessage
或structuredClone
。 -
你只发送一个字符串(而不是混合数据)。
-
字符串不是子字符串、绳状字符串、原子或符号。
-
你将字符串发送到同一进程中的另一个线程。
-
字符串长度 ≥ 256 字符。
这涵盖了在工作线程之间发送字符串的极其常见的模式。
总结
通过识别哪些字符串可以安全地共享而无需序列化,Bun 在 postMessage(string)
上实现了显著的性能提升。这种优化对于多线程 JavaScript 服务器和命令行工具尤其有用。