最近正在做一个基于浏览器File Reader API的文本解析工具。为了让繁重的文本解析工作不影响页面线程的性能,使用Web Worker来负责处理文本解析应该是最优的。Web Worker需要将解析后的每行文本传回给页面,当需要传输的文本行数达到数十万、百万行时,性能问题就变得尤为重要,一方面是传输的耗时,一方面是内存的消耗。多数浏览器实现了结构克隆,允许你对Web Worker传入、传出更复杂的数据类型,如:File, Blob, ArrayBuffer, JSON对象等。然而当你使用postMessage()
方法传输这些数据时,数据会被拷贝一份再进行传输,所以当你传输100MB的数据时,主进程和Worker进程都会增加100MB的内存使用,并且复制100MB的数据需要的时间可能达到几百毫秒。为了解决这个问题,postMessage()
方法也支持传输Transferable数据类型,使用Transferable传输时,会直接把数据从一个执行环境(Worker线程或主线程)传输到另一个执行环境,这样不会额外增加一份内存消耗,并且传输速度极快因为不需要数据拷贝。可是在实际使用中,如果需要传输大量的Transferable数据时,这种方法仍存在显著的性能问题。
拷贝传输
首先我们来看最简单的使用Web Worker直接传输500MB的数据。
var data = new Uint8Array(500 * 1024 * 1024);
self.postMessage(data);
浏览器的测试结果:
浏览器 | 传输耗时 | 最终内存 |
---|---|---|
Chrome | 149ms | 1042MB |
Edge | 455ms | 1048MB |
Firefox | 380ms | 1079MB |
传输后浏览器进程内存增长到了1GB,因为data
对象被复制了一份传输到主进程,传输后我们在worker进程和主进程都可以访问到这500MB的数据。
Transferable对象传输
接下来我们来使用Transferable对象传输500MB数据。
var data = new Uint8Array(500 * 1024 * 1024);
self.postMessage(data, [data.buffer]);
浏览器的测试结果:
浏览器 | 传输耗时 | 最终内存 |
---|---|---|
Chrome | 1ms | 531MB |
Edge | 0ms | 537MB |
Firefox | 0ms | 549MB |
使用Transferable传输后,内存始终维持在500MB左右,这是因为data
数据从worker进程传到了主进程,这时候你在worker上下文中执行data.length
会得到0。因为不需要复制,所以postMessage()
执行非常快。
大量Transferable对象的传输问题
根据上述测试结果,当需要传输单一一个大数据变量时,如果在原有的上下文环境中不再需要访问该对象时,那么使用Transferable对象传输是一个明智的选择。
然而我最近在做的一个文本解析的库,需要在Web Worker中解析出文本文件中的每一行,然后把若干行数据返回到主进程。当返回的行数特别多时,我发现Transferable传输的性能会显著下降。
看下面的测试代码,transferByLine
方法接受2个参数,第一个是测试的行数,第二个是是否使用Transferable传输。
function transferByLine(line, transferable) {
var arr = [];
var bufferArr = [];
for (var i = 0; i < line; i++) {
arr[i] = new Uint8Array(100);
if (transferable) {
bufferArr.push(arr[i].buffer);
}
}
console.log('Successfully created the array. The array has ' + line + ' items, each item size is 100 bytes');
console.log('Start transferring...');
var startTime = new Date().getTime();
if (!transferable) {
self.postMessage(arr);
} else {
self.postMessage(arr, bufferArr);
}
var timeTaken = new Date().getTime() - startTime;
console.log('Tranfer completed in ' + timeTaken + 'ms.');
}
下面是我在三个浏览器中的测试结果。
2000行 | 4000行 | 6000行 | 8000行 | 10000行 | 20000行 | 40000行 | 60000行 | 80000行 | 100000行 | 200000行 | |
---|---|---|---|---|---|---|---|---|---|---|---|
Chrome (拷贝) | 1 | 2 | 2 | 3 | 5 | 9 | 36 | 40 | 53 | 65 | 170 |
Chrome (Transferrabl输) | 3 | 12 | 21 | 35 | 44 | 109 | 383 | 789 | 1408 | 2015 | 7609 |
Edge (拷贝) | 1 | 3 | 3 | 3 | 4 | 8 | 15 | 22 | 27 | 46 | 108 |
Edge (Transferable) | 6 | 19 | 37 | 63 | 88 | 329 | 1266 | 2814 | 4940 | 7759 | 30341 |
Firefox (拷贝) | 2 | 5 | 8 | 10 | 14 | 28 | 58 | 96 | 149 | 174 | 362 |
Firefox (Transferable) | 2 | 5 | 7 | 10 | 14 | 34 | 64 | 122 | 174 | 222 | 488 |
将测试结果做成图表可以更直观地观察性能随着行数的变化情况:
从上述测试结果可以看出:
- Chrome和Edge的Transferable传输性能会随着数组元素的增加呈指数级别下降,基本只要数组长度上千,性能就不如拷贝传输了。
- Firefox的Transferable传输性能变化与拷贝传输基本的变化相当,都是呈线性增长。
当Tranferrable对象过多时,Chrome和Edge应该是慢在花很多时间来解析Transferable数组成员,来和postMessage()
方法中的第一个参数的数据成员做一一关系映射。很显然,Firefox应该是对这块逻辑做过相应的优化。
总结
当Web Worker需要传输大量数据时,如果数据集中于少数变量中,那么可以放心地使用Transferable来传输。如果数据分散于成百上千个元素中,这时候使用Transferable传输在Chrome和Edge中会有比较明显的性能问题,用拷贝传输反而更快。如果使用了拷贝传输,我们需要在Web Worker中断开其它JS变量对数据源的引用,这样才可以使浏览器在下一次垃圾回收时释放Web Worker中数据源所占用的内存。