实测告诉你:Web Worker 传输大字符串,哪种方法才不掉坑?

1,957 阅读5分钟

前段时间,我写过一篇文章 如何在 Web Worker 中高效传输大批量数据(>10MB)。这篇文章是该文章的姊妹篇。

在上一篇文章中,我们介绍了 Web 开发中,Web Worker 是提升性能的重要工具。它能让我们把计算密集型任务放到后台线程执行,从而避免阻塞 UI。但 主线程和 Worker 之间的通信成本,常常成为性能瓶颈。所以,最佳方案是使用 Transferable Objects(可转移对象,如 ArrayBufferMessagePortImageBitmap),它可以在 postMessage 时通过 转移(transfer)而非复制来传输。这样可以 零拷贝,避免大的开销。

但是,如果要传输给 Web Worker 的数据本身不是 可转移对象,这个时候我们就需要将这些数据编码为 ArrayBuffer,然后在Web WorkerArrayBuffer 的数据进行解码。如果编码和解码的成本比较高,那么使用 可转移对象 是否还是一个正确的选择吗?

在最近的工作中我就碰到了这样的问题,所以我就写了一个基准测试的程序来寻求答案。实际的测试结果和原先的预测有非常大的差异。

🔍 Web Worker通信机制的原理

在查看具体的测试结果前,我们先回顾一下Web Worker通信机制的原理。Web Worker 与主线程之间通信依赖于 postMessage API

默认方式:结构化克隆(Structured Clone Algorithm)

当我们调用:

worker.postMessage(largeObject);

浏览器会使用 结构化克隆算法largeObject 完整复制 一份给 Worker:

  • 不是共享内存,而是克隆副本
  • CPU 开销大:对象内容需要遍历、复制
  • 内存开销大:同一份数据在主线程和 Worker 中会存在两份

对于 几十 MB 甚至上百 MB 的大对象,这种复制可能导致明显的性能瓶颈。

最佳实践:可转移对象(Transferable Objects)

为了解决这个问题,浏览器支持一类特殊的数据类型 —— Transferable Objects
常见的可转移对象有:

  • ArrayBuffer
  • MessagePort
  • ImageBitmap

这类对象在 postMessage 时可以通过 “转移” 而不是复制来传输:

// 转移 ArrayBuffer
worker.postMessage(buffer, [buffer]);

特点:

  • 零拷贝:数据的所有权直接转移到 Worker
  • 避免 GC 压力:主线程的 buffer 立即失效,不再占内存
  • 高效:非常适合传输二进制数据(例如音视频流、图片、typed array)

🤔 那字符串呢?

字符串本身并不是 Transferable Object。因此,如果要借助可转移机制,我们必须先把字符串 编码为 ArrayBuffer,再传输,最后再解码回来。

于是,就有了本文的两个对比方案:

  1. Direct String Transfer —— 直接传字符串(结构化克隆)
  2. ArrayBuffer Transfer —— 字符串转 ArrayBuffer,再零拷贝传输

从直觉上看,方法 2 应该更快(因为避免了拷贝),但实际测试的结果却给出了完全不同的答案。

🚀 Benchmark 测试

我写了一个基准测试项目,开源在 GitHub 👉 mlight-lee/web-worker-string-benchmark

📊 测试方法

方法一:Direct String Transfer

  • 使用 postMessage(string) 直接发送字符串
  • 浏览器用结构化克隆算法处理
  • 不涉及额外的编码/解码

方法二:ArrayBuffer Transfer

  • 使用 TextEncoder 把字符串转成 ArrayBuffer
  • postMessage(buffer, [buffer]) 零拷贝传递
  • 在 worker 中用 TextDecoder 解码回字符串
  • 包含额外的 编码/解码开销

🎯 测试参数

  • 字符串大小:1MB, 5MB, 10MB, 50MB, 100MB
  • 字符串类型:ASCII 随机字符串、Unicode (UTF-8 编码) 字符串
  • 运行次数:每种情况 20 次,取平均值、最小值、最大值、中位数
  • 浏览器版本:Google Chrome 139

📈 测试结果

这是我在自己 Windnows 笔记本的 Chrome浏览器 上做的测试的测试结果。在不同机器和浏览器上测试,测试结果可能会有所差异。但是,我在 Edge 139 测试下来,结论基本一致。

ASCII 字符串

传输的字符串只包含ASCII码大于等于32、小于等于127的字符。

大小直接传 (平均)ArrayBuffer (平均)更快方法相对提升
1 MB3.90 ms9.42 msDirect58.6%
5 MB13.70 ms39.83 msDirect65.6%
10 MB22.40 ms66.58 msDirect66.3%
50 MB93.11 ms338.79 msDirect72.5%
100 MB211.33 ms692.54 msDirect69.5%

Unicode 字符串

传输的字符串包含了Unicode编码范围内的任意字符。

大小直接传 (平均)ArrayBuffer (平均)更快方法相对提升
1 MB2.92 ms16.57 msDirect82.3%
5 MB11.99 ms80.48 msDirect85.1%
10 MB26.67 ms166.73 msDirect84.0%
50 MB93.90 ms700.85 msDirect86.6%
100 MB202.35 ms1529.89 msDirect86.8%

🔍 结果分析

  1. 编码/解码成本过高

    • TextEncoder / TextDecoder 的开销非常大,尤其是 Unicode 字符串。
    • 这使得 ArrayBuffer 的零拷贝优势完全被抵消。
  2. 字符串越大,差距越明显

    • 直接传递在 50MB~100MB 的场景下依然保持 70% 左右的性能优势。
  3. ASCII vs Unicode

    • 对于 ASCII,差距在 60–70%
    • 对于 Unicode,差距更大,直接传递可快 7–8 倍
  4. 内存开销对比

    • 直接传:大约 2 倍内存(原始 + 拷贝)
    • ArrayBuffer:大约 3 倍内存(原始 + 编码后的字节数组 + 解码后的字符串)

✅ 结论与建议

故事到这里也就有了一个反转:一开始我以为 ArrayBuffer 会是更优解,但经过实际测试发现 —— 直接传字符串才是王道。这让我更坚信一句老话:性能优化不能只靠直觉,一定要用数据说话!

  • 赢家:🚀 Direct String Transfer (postMessage(string))

  • 适用场景

    • 如果数据本身就是字符串,直接传递更快、更简单。
    • 除非你已经有二进制数据(例如图片、TypedArray),否则不建议多此一举用 ArrayBuffer。
  • 一句话总结
    👉 别为了零拷贝而强行转码字符串,结果可能更慢还更费内存。