一,postMessage 的性能问题
postMessage
依赖于结构化克隆数据,将消息从一个 JavaScript 空间复制到另一个 JavaScript 空间。
1.1 结构化克隆算法
- 在消息上执行
StructuredSerialize()
- 在接收方中任务队列中加入一个任务,该任务将执行以下步骤
- 在序列化的消息上执行
StructuredDeserialize()
- 创建一个
MessageEvent
并派发一个带有该反序列化消息的MessageEvent
事件到接收端口上。
- 在序列化的消息上执行
StructuredSerialize()
和 StructuredDeserialize()
在实际场景中并不是真正的函数,因为它们不是通过 JavaScript 暴露出去的。
那这两个函数实际上是做什么的呢?现在,你可以将 StructuredSerialize() 和 StructuredDeserialize() 视为 JSON.stringify() 和 JSON.parse() 的智能版本。
从处理循环数据结构、内置数据类型(如 Map、Set和ArrayBuffer)等方面来说,它们更聪明。
传输时间与 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);
浏览器的测试结果:
传输后浏览器进程内存增长到了 1GB,因为 data 对象被复制了一份传输到主进程,传输后我们在 worker 进程和主进程都可以访问到这 500MB 的数据。
二,使用 Transferable 传输
postMessage 方法也支持传输 Transferable 数据类型,使用 Transferable 传输时,会直接把数据从一个执行环境(Worker 线程或主线程)传输到另一个执行环境。这样不会额外增加一份内存消耗,并且传输速度极快因为不需要数据拷贝。
可是在实际使用中,如果需要传输大量的 Transferable 数据时,这种方法仍存在显著的性能问题。
接下来我们来使用 Transferable 对象传输500MB数据。
var data = new Uint8Array(500 * 1024 * 1024);
self.postMessage(data, [data.buffer]);
浏览器的测试结果:
使用 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