将一个大的 blob 复制给一个 worker 是不是很昂贵?

Is copying a large blob over to a worker expensive?

使用 Fetch API 我能够对大量二进制数据资产(比如超过 500 MB)发出网络请求,然后将 Response 转换为 BlobArrayBuffer.

之后,我可以执行 worker.postMessage 并让标准结构化克隆算法将 Blob 复制到 Web Worker 或将 ArrayBuffer 传输到 worker 上下文(使实际上不再可从主线程获得)。

起初,似乎以 ArrayBuffer 形式获取数据会更可取,因为 Blob 不可转移,因此需要复制过来。但是,blob 是不可变的,因此,浏览器似乎没有将其存储在与页面关联的 JS 堆中,而是存储在专用的 blob 存储 space 中,因此,最终被复制到worker 上下文只是一个参考。

我准备了一个演示来尝试两种方法之间的区别:https://blobvsab.vercel.app/。我正在使用这两种方法获取 656 MB 的二进制数据。

我在本地测试中观察到的一些有趣的事情是,复制 Blob 甚至比传输 ArrayBuffer:

Blob从主线程复制到worker的时间:1.828125 ms

ArrayBuffer从主线程到worker的传输时间:3.393310546875 ms

这是一个强有力的指标,表明处理 Blob 实际上非常便宜。由于它们是不可变的,浏览器似乎足够聪明,可以将它们视为参考,而不是将覆盖的二进制数据链接到这些参考。

以下是我作为 Blob:

获取时拍摄的堆内存快照

前两个快照是在使用 postMessage 将提取结果 Blob 复制到工作上下文后拍摄的。请注意,这些堆都不包含 656 MB。

后两个快照是在我使用 FileReader 实际访问底层数据后拍摄的,正如预期的那样,堆增长了很多。

现在,这就是直接作为 ArrayBuffer:

获取时发生的情况

在这里,由于二进制数据只是通过工作线程传输的,因此主线程的堆很小,但工作堆包含全部 656 MB,甚至在读取此数据之前。

现在,环顾 SO 我发现 What is the difference between an ArrayBuffer and a Blob? 提到了两种结构之间的许多潜在差异,但我还没有找到关于是否应该担心复制 Blob 在执行上下文与 ArrayBuffer 的内在优势之间,它们是可转移的。但是,我的实验表明复制 Blob 实际上可能更快,因此我认为更可取。

这似乎取决于每个浏览器供应商如何存储和处理 Blob。我发现 this Chromium documentation 描述所有 Blobs 都从每个渲染器进程(即选项卡上的页面)传输到浏览器进程,这样 Chrome 甚至可以卸载 Blob 到辅助内存(如果需要)。

有没有人对这一切有更多的见解?如果我可以选择通过网络获取一些大型二进制数据并将其移动到 Web Worker,我应该更喜欢 Blob 还是 ArrayBuffer

不,postMessage 一个 Blob 一点都不贵。

Blob 的 cloning steps

Their serialization steps, given value and serialized, are:

  1. Set serialized.[[SnapshotState]] to value’s snapshot state.

  2. Set serialized.[[ByteSequence]] to value’s underlying byte sequence.

Their deserialization step, given serialized and value, are:

  1. Set value’s snapshot state to serialized.[[SnapshotState]].

  2. Set value’s underlying byte sequence to serialized.[[ByteSequence]].

换句话说,没有复制任何东西,快照状态和字节序列都是通过引用传递的,(即使包装 JS 对象不是)。

但是关于您的整个项目,我不建议在这里使用 Blob,原因有二:

  1. 获取算法首先在内部作为 ArrayBuffer 获取。请求 Blob 会在那里添加一个额外的步骤(这会消耗内存)。
  2. 您可能需要从 Worker 读取该 Blob,再添加一个步骤(这也会消耗内存,因为此处数据实际上会被复制)。