Transferable Objects: Moving ArrayBuffers Instead of Copying in postMessage
AI generated
JS
() =>
JavaScript · Web Workers · Performance
Transferable Objects: Moving Memory Instead of Copying It
ArrayBuffer.prototype.transfer() and postMessage transfer lists for fast data exchange with web workers

Structured clone copies data by default on every postMessage call, which costs noticeable time with large binary data. Transferable objects solve this through an ownership change instead of a copy. This article covers the postMessage transfer list, the newer ArrayBuffer.prototype.transfer(), and common pitfalls with detached buffers.

15 min read Web Workers ArrayBuffer Performance

1. The problem: expensive copying of large binary data

When data is exchanged via postMessage between the main thread and a web worker, the browser uses the structured clone algorithm by default, which creates a full copy of the passed data. For small objects this barely matters, but for large ArrayBuffers, for example image, audio, or sensor data in the megabyte to gigabyte range, the copy becomes a measurable performance problem.

The cost of copying scales directly with the data size and also briefly blocks the thread sending the message. It is exactly for this scenario that the concept of transferable objects was introduced, allowing an object's memory to be handed to the receiver without a copy.

2. Structured clone as the default behavior

Without an explicit transfer list, postMessage fully copies every ArrayBuffer passed to it. The result is that both the main thread and the worker afterward each hold their own, independent copy of the same data, which briefly doubles memory usage and increases transfer time proportionally to the data size.

For most use cases with small messages, this behavior is entirely sufficient and even safer, because both sides can work independently with their own copy without affecting each other. Switching to transfer only pays off once larger amounts of data are involved.


// main thread: default copy without a transfer list
const buffer = new ArrayBuffer(64 * 1024 * 1024); // 64 MB
const worker = new Worker("worker.js");

console.time("postMessage-copy");
worker.postMessage({ buffer });
console.timeEnd("postMessage-copy");
// buffer stays fully usable in the main thread,
// the worker receives a complete copy.

3. Transferable objects: ownership transfer instead of a copy

postMessage accepts a transfer list as its second parameter, an array of objects whose underlying memory is not copied but whose ownership is handed to the receiver. After the transfer, only the receiver has access to the data, the sender loses it entirely.

Transferable objects include ArrayBuffer, MessagePort, ImageBitmap, and OffscreenCanvas. TypedArrays like Uint8Array themselves are not directly transferable, but their underlying .buffer is, which is why with TypedArrays the buffer specifically needs to go into the transfer list, not the view itself.

4. Practical example: transferring a large buffer

In the following example, a large ArrayBuffer is handed to a worker via the transfer list. The decisive difference from the default behavior is the second parameter of postMessage, which explicitly states which objects should be transferred instead of copied.

After the call, the buffer is detached in the main thread, its byteLength is 0, and any read access on old TypedArray views throws no error but no longer returns any data. This is a deliberate safety feature that prevents two threads from uncontrollably modifying the same memory region at the same time.


// main thread
const buffer = new ArrayBuffer(64 * 1024 * 1024);
const worker = new Worker("worker.js");

worker.postMessage({ buffer }, [buffer]);

console.log(buffer.byteLength); // -> 0, buffer is now detached

// worker.js
self.onmessage = (event) => {
  const { buffer } = event.data;
  console.log(buffer.byteLength); // -> 67108864, full access in the worker
};

5. ArrayBuffer.prototype.transfer(): explicit transfer in the same context

ECMAScript 2024 added ArrayBuffer.prototype.transfer(), a method that creates a new ArrayBuffer taking over the memory content of the original buffer, while the original buffer is immediately detached. Unlike a postMessage transfer, this also works within the same thread, for example to hand a buffer to another function while the calling function loses access to it afterward.

The transferToFixedLength() variant additionally forces the new buffer to no longer be resizable, even if the original buffer was created as resizable with maxByteLength. Both methods optionally accept a new byte length as a parameter, letting the resulting buffer be grown or shrunk at the same time as the transfer.


const original = new ArrayBuffer(16);
new Uint8Array(original).set([1, 2, 3, 4]);

const moved = original.transfer(); // ownership changes
console.log(original.byteLength);  // -> 0, original is detached
console.log(moved.byteLength);     // -> 16, data has been taken over
console.log(new Uint8Array(moved)); // -> Uint8Array(16) [1, 2, 3, 4, 0, ...]

6. Detached buffers as a common source of bugs

The most common bug around transferable objects happens when an old reference is accidentally accessed after the transfer, for example because a TypedArray view on the old buffer was cached in a closure. The access does not throw a runtime error, but silently returns empty or unexpected data, which makes debugging harder.

A good debugging approach is to explicitly check and log before every transfer whether a buffer is already detached, recognizable by a byteLength of 0 combined with the knowledge that the buffer previously had a larger length. Consistently clearing all references to the old buffer right after the transfer reliably prevents this class of bugs.


function safeTransfer(buffer) {
  if (buffer.byteLength === 0) {
    throw new Error("Buffer is already detached, cannot transfer again");
  }
  return buffer.transfer();
}

const buf = new ArrayBuffer(8);
const moved = safeTransfer(buf);
try {
  safeTransfer(buf); // -> throws, buf.byteLength is already 0
} catch (error) {
  console.error(error.message);
}

7. Performance: when the switch pays off

With small amounts of data in the kilobyte range, the difference between copy and transfer is barely measurable, and the effort of an explicit transfer list usually is not worth it. From a few megabytes of data, for example video frames, audio buffers, or larger sensor datasets, the difference becomes clearly noticeable, since transfer takes nearly constant time instead of growing linearly with the size.

With very large amounts of data in the hundreds of megabytes or gigabyte range, a structured clone copy can also briefly cause noticeable stutter in the user interface, while a transfer completes practically instantly, since only a pointer to the same memory region is passed, no bytes are actually copied.

8. Practical case: a processing pipeline across multiple workers

A typical practical case is image or audio processing in a pipeline of several workers, where raw data is passed from one worker to the next, for example for decoding, filtering, and subsequent encoding. Without transfer, every step would incur a full copy, unnecessarily extending the total runtime of the pipeline.

Combined with OffscreenCanvas, rendering itself can even be offloaded to a worker, and a finished ImageBitmap can be handed back to the main thread via transfer, allowing expensive image processing to run outside the UI thread without an expensive copy of the result at the end.


// main thread
const offscreen = canvas.transferControlToOffscreen();
const renderWorker = new Worker("render-worker.js");
renderWorker.postMessage({ canvas: offscreen }, [offscreen]);

// render-worker.js
self.onmessage = (event) => {
  const ctx = event.data.canvas.getContext("2d");
  ctx.fillStyle = "blue";
  ctx.fillRect(0, 0, 100, 100); // rendering runs in the worker
};

9. Limits and recommendation

It is important to know that after a transfer, the original buffer is no longer usable in the sending context, meaning it is no longer available there for its own computations. If the buffer is still needed in the sender afterward, a real copy must be created instead and only that copy transferred.

As a recommendation: for small, occasional messages, structured clone is entirely sufficient. Once large amounts of binary data are moved repeatedly between threads, for example in streaming or processing pipelines, transferable objects should be used consistently, combined with clear ownership of which thread holds the data at any given time.

Object / API Transferable? Behavior After Transfer Typical Use
ArrayBuffer Yes byteLength becomes 0, detached Binary data, sensor and media data
MessagePort Yes Port no longer usable in the sender Direct communication channels between workers
ImageBitmap Yes Bitmap no longer usable in the sender Image data between worker and main thread
OffscreenCanvas Yes Canvas control moves to the receiver Rendering outside the UI thread
TypedArray View No, only .buffer is transferable The view itself is not transferred Access to ArrayBuffer contents

Mironsoft

Modern browser APIs, performance, and maintainable JavaScript

JavaScript that holds up in the real browser, not just in the tutorial?

We review existing frontend code for outdated patterns, unnecessary dependencies, and performance traps, then replace them with modern, native browser APIs that mean less bundle weight and less maintenance burden.

Code Review

Systematically finding outdated patterns, unnecessary dependencies, and memory leaks.

Performance Optimization

Improving bundle size, load time, and runtime performance with modern APIs.

Modernization

Deliberately introducing native browser APIs instead of heavy libraries.

10. Summary

Transferable Objects: The Essentials at a Glance

Structured Clone

Default behavior of postMessage, fully copies data, cost scales with size

Transfer List

Second parameter of postMessage, transfers ownership instead of copying

ArrayBuffer.transfer()

ES2024 method for explicit transfer even within the same thread

Detached Buffer

byteLength becomes 0, any further use in the sender is excluded

11. FAQ: Transferable Objects: The Essentials at a Glance

1What is a transferable object in JavaScript?
A transferable object is an object like ArrayBuffer, MessagePort, ImageBitmap, or OffscreenCanvas whose underlying memory can be handed to the receiver via ownership transfer instead of being copied by postMessage.
2How does transfer differ from structured clone?
Structured clone fully copies the data, so sender and receiver hold independent copies. Transfer only moves ownership of the same memory region without copying the data.
3How do I use the transfer list with postMessage?
The transfer list is passed as the second parameter of postMessage, an array of the objects to transfer, for example worker.postMessage({ buffer }, [buffer]).
4What happens to an ArrayBuffer after the transfer?
The original ArrayBuffer becomes detached, its byteLength becomes 0, and it can no longer be used for read or write access in the sending context.
5What does ArrayBuffer.prototype.transfer() do?
This method, available since ES2024, creates a new ArrayBuffer that takes over the content of the original buffer, while the original buffer is immediately detached, even within the same thread.
6Are TypedArrays like Uint8Array directly transferable?
No, TypedArrays themselves are not transferable, but their underlying .buffer is. For a TypedArray, array.buffer must therefore be included in the transfer list.
7From what data size does transfer pay off over copying?
With small amounts of data in the kilobyte range, the difference is barely measurable. From a few megabytes, for example with image or audio data, the performance gain becomes clearly noticeable.
8What is a common mistake when working with transferable objects?
The most common mistake is accessing an old reference after the transfer, which throws no runtime error but silently returns empty or unexpected data.
9Can I transfer an already detached buffer again?
No, an already detached buffer has a byteLength of 0 and no longer contains any data, transferring it again makes no sense and should be checked explicitly beforehand.
10What is OffscreenCanvas combined with transfer good for?
OffscreenCanvas allows rendering work to be offloaded to a web worker and canvas control to be handed to the worker via transferControlToOffscreen and the transfer list, without blocking the UI thread.