使用 Uint8Array 优化 postMessage

407 阅读3分钟

一,postMessage 的性能问题

postMessage 依赖于结构化克隆数据,将消息从一个 JavaScript 空间复制到另一个 JavaScript 空间。

1.1 结构化克隆算法

  1. 在消息上执行 StructuredSerialize()
  2. 在接收方中任务队列中加入一个任务,该任务将执行以下步骤
    1. 在序列化的消息上执行 StructuredDeserialize()
    2. 创建一个 MessageEvent  并派发一个带有该反序列化消息的 MessageEvent 事件到接收端口上。

StructuredSerialize()StructuredDeserialize() 在实际场景中并不是真正的函数,因为它们不是通过 JavaScript 暴露出去的。
那这两个函数实际上是做什么的呢?现在,你可以将 StructuredSerialize() 和 StructuredDeserialize() 视为 JSON.stringify() 和 JSON.parse() 的智能版本
从处理循环数据结构、内置数据类型(如 Map、Set和ArrayBuffer)等方面来说,它们更聪明。

BF00CA5F-D7AC-463B-B967-2449417405B9.webp

传输时间与 JSON.stringify() 返回的字符串长度有很强的相关性。

我认为这种相关性足够强,可以给出一个经验法则:对象的 JSON 字符串化后的大小大致与它的传输时间成正比。 然而,更需要注意的事实是,这种相关性只与大对象相关,我说的大是指超过 100 KiB 的任何对象。

1.2 内存占用

多数浏览器实现了结构克隆,允许你对 Web Worker 传入、传出更复杂的数据类型,如:File, Blob, ArrayBuffer, JSON 对象等。

然而当你使用 postMessage() 方法传输这些数据时,数据会被拷贝一份再进行传输,所以当你传输 100MB 的数据时,主进程和 Worker 进程都会增加 100MB 的内存使用,并且复制 100MB 的数据需要的时间可能达到几百毫秒。

首先我们来看最简单的使用 Web Worker 直接传输 500MB 的数据。

var data = new Uint8Array(500 * 1024 * 1024); 
self.postMessage(data);

浏览器的测试结果:

508C0A75-CC31-41F9-B811-B4B9331952B0.png

传输后浏览器进程内存增长到了 1GB,因为 data 对象被复制了一份传输到主进程,传输后我们在 worker 进程和主进程都可以访问到这 500MB 的数据。

二,使用 Transferable 传输

postMessage 方法也支持传输 Transferable 数据类型,使用 Transferable 传输时,会直接把数据从一个执行环境(Worker 线程或主线程)传输到另一个执行环境。这样不会额外增加一份内存消耗,并且传输速度极快因为不需要数据拷贝。

可是在实际使用中,如果需要传输大量的 Transferable 数据时,这种方法仍存在显著的性能问题。

接下来我们来使用 Transferable 对象传输500MB数据。

var data = new Uint8Array(500 * 1024 * 1024);
self.postMessage(data, [data.buffer]);

浏览器的测试结果:

680B85E9-BC4C-4084-B102-8B153462215D.png

使用 Transferable 传输后,内存始终维持在 500MB 左右,这是因为 data 数据从 worker 进程传到了主进程,这时候你在 worker 上下文中执行 data.length 会得到 0。因为不需要复制,所以 postMessage 执行非常快。

三,在 IE11 下使用 Uint8Array 传输

JavaScript 中的对象可以通过使用 ArrayBuffer 构造函数和一些其他 API 来转换为 ArrayBuffer

3.1 转码

function textEncode(str) {
  	// 非 IE11 使用 TextEncoder 转码
    if (window.TextEncoder) {
      return new TextEncoder('utf-8').encode(str)
    }
  	// IE11 下使用 encodeURIComponent
    var utf8 = unescape(encodeURIComponent(str))
    var result = new Uint8Array(utf8.length)
    for (var i = 0; i < utf8.length; i++) {
      result[i] = utf8.charCodeAt(i)
    }
    return result
}


const object = { foo: 'bar' };
// 将序列化对象转为字符串
const serializedObject = JSON.stringify(object);
// 将字符串转为 Uint8Array 格式
const data = textEncode(serializedObject)

// 第三个参数在 win7 的 IE11 下有兼容问题,不建议使用
window.postMessage(data, '/', [data.buffer])

3.2 解码

function decodeUtf8(bytes) {
  var encoded = ''
  for (var i = 0; i < bytes.length; i++) {
    encoded += '%' + bytes[i].toString(16)
  }
  return decodeURIComponent(encoded)
}

window.addEventListener('message', (event) => {
	if (event && event.data && event.data instanceof Uint8Array) {
    	let storeObject = JSON.parse(decodeUtf8(event.data)); 
      	// { foo: 'bar' }
    }
})

四,参考文章

js object转arraybuffer-掘金 (juejin.cn)

javascript - How do I use TextEncoder in IE11? - Stack Overflow

Web Worker传输大量Transferable对象时的性能问题 (joji.me)

[译] postMessage 很慢吗? - 掘金 (juejin.cn)