Transferable Objects in JavaScript: Zero-Copy postMessage

tutorialMarch 22, 2026ยท 5 min read

When you send data between a Web Worker and the main thread using postMessage, the browser copies the data. For small objects, this is fine. For a 50MB image buffer or a large typed array, copying is expensive โ€” it blocks the thread and wastes memory.

Transferable Objects solve this. Instead of copying, the browser moves the data โ€” transferring ownership from one context to another in constant time, regardless of size.

The Problem: Structured Cloning is Slow for Large Data

By default, postMessage uses the structured clone algorithm to serialize and deserialize data. This is a deep copy โ€” similar to JSON.parse(JSON.stringify(data)) but more powerful (handles circular refs, typed arrays, etc.).

For a 100MB ArrayBuffer, that's 100MB being copied across thread boundaries. In a real app โ€” video processing, audio, image manipulation, or machine learning โ€” this adds up fast.

// worker.js
const bigBuffer = new ArrayBuffer(100 * 1024 * 1024); // 100MB
// This copies the entire 100MB โ€” slow!
self.postMessage({ buffer: bigBuffer });

The Solution: Transfer Instead of Copy

Transferable Objects are types that support a "transfer" operation โ€” the browser moves them to the new context and detaches (invalidates) them in the original context. The data isn't copied; ownership is transferred.

// worker.js
const bigBuffer = new ArrayBuffer(100 * 1024 * 1024); // 100MB

// Pass the buffer in the transferables list (second argument)
self.postMessage({ buffer: bigBuffer }, [bigBuffer]);

// bigBuffer is now detached โ€” accessing it here throws
console.log(bigBuffer.byteLength); // 0
// main.js
worker.onmessage = (event) => {
  const buffer = event.data.buffer;
  console.log(buffer.byteLength); // 104857600 โ€” the full 100MB, instantly
};

The transfer is O(1) โ€” it's just a pointer swap in memory. A 1MB buffer and a 1GB buffer transfer in the same time.

What Types Are Transferable?

Not all objects are transferable. The browser only supports transfer for specific types:

TypeUse case
ArrayBufferRaw binary data, typed arrays
MessagePortMessageChannel endpoints
ImageBitmapDecoded images
OffscreenCanvasCanvas rendering off the main thread
ReadableStreamStreaming data
WritableStreamStreaming output
TransformStreamStream transformation

Plain objects, strings, arrays, and numbers are not transferable โ€” they always get structured-cloned.

Typed Arrays and ArrayBuffer

ArrayBuffer is the most commonly transferred type. Note that typed arrays like Float32Array or Uint8Array are views on an ArrayBuffer โ€” you transfer the underlying buffer, not the view:

const float32 = new Float32Array(1000);
float32.fill(3.14);

// Transfer the underlying ArrayBuffer
worker.postMessage({ data: float32 }, [float32.buffer]);

// float32 is now detached
console.log(float32.byteLength); // 0

On the receiving end, you can reconstruct the typed array view:

// worker.js
self.onmessage = (event) => {
  const float32 = new Float32Array(event.data.data.buffer);
  console.log(float32[0]); // 3.14
};

Transferring ImageBitmap for Fast Image Processing

ImageBitmap is perfect for offloading image processing to a worker without copying pixel data:

// main.js
const response = await fetch('/photo.jpg');
const blob = await response.blob();
const bitmap = await createImageBitmap(blob);

// Transfer the bitmap to a worker โ€” zero copy
worker.postMessage({ image: bitmap }, [bitmap]);
// bitmap is now detached here
// worker.js
self.onmessage = (event) => {
  const bitmap = event.data.image;
  // Process the image โ€” apply filters, resize, etc.
  // Then transfer results back
};

OffscreenCanvas: Render Off the Main Thread

OffscreenCanvas is one of the most powerful transferable types. Transfer a canvas to a worker, and the worker can render to it directly โ€” keeping your main thread completely unblocked:

// main.js
const canvas = document.getElementById('my-canvas');
const offscreen = canvas.transferControlToOffscreen();

worker.postMessage({ canvas: offscreen }, [offscreen]);
// worker.js
self.onmessage = (event) => {
  const canvas = event.data.canvas;
  const ctx = canvas.getContext('2d');

  // Render complex visuals here โ€” main thread stays responsive
  ctx.fillStyle = 'blue';
  ctx.fillRect(0, 0, canvas.width, canvas.height);
};

Using Transferables with BroadcastChannel

Transferable Objects also work with the BroadcastChannel API. The same second-argument syntax applies:

const channel = new BroadcastChannel('data-pipe');
const buffer = new ArrayBuffer(10 * 1024 * 1024); // 10MB

channel.postMessage({ buffer }, [buffer]);
// buffer is transferred to the first receiving tab

One caveat: with BroadcastChannel, the transfer goes to one receiver (the first one to accept it). Other tabs that are also listening will receive a detached buffer. This is usually fine if only one tab needs to process the data.

Using Transferables with MessageChannel

MessageChannel creates a pair of MessagePort objects โ€” and MessagePort itself is a Transferable. This is how you pass a communication channel through postMessage:

const { port1, port2 } = new MessageChannel();

// Transfer port2 to a worker so it can communicate directly
worker.postMessage({ port: port2 }, [port2]);

// Now communicate via port1
port1.postMessage('hello from main thread');
// worker.js
self.onmessage = (event) => {
  const port = event.data.port;
  port.onmessage = (e) => {
    console.log(e.data); // 'hello from main thread'
    port.postMessage('hello back');
  };
};

Transferring Multiple Objects

You can transfer multiple objects in one call โ€” just list them all in the transfer array:

const buffer1 = new ArrayBuffer(1024);
const buffer2 = new ArrayBuffer(2048);
const bitmap = await createImageBitmap(blob);

worker.postMessage(
  { buffer1, buffer2, bitmap },
  [buffer1, buffer2, bitmap]  // all three transferred
);

When to Use Transferables vs Structured Clone

ScenarioUse
Small objects, strings, numbersStructured clone (default) โ€” fine
Large binary data (images, audio, video frames)Transfer ArrayBuffer
Passing a canvas to a workerTransfer OffscreenCanvas
Setting up a direct channel between two workersTransfer MessagePort
Streaming dataTransfer ReadableStream / WritableStream

The rule of thumb: if the data is bigger than ~1MB and you don't need it in the sending context after the transfer, use a Transferable.

Common Gotcha: Accessing Transferred Data

Once you transfer an object, it's gone from the sending context. Any attempt to use it throws a DataCloneError or returns byteLength: 0:

const buffer = new ArrayBuffer(1024);
worker.postMessage({ buffer }, [buffer]);

// โŒ Don't do this โ€” buffer is detached
const view = new Uint8Array(buffer); // byteLength is 0, data is gone

If you need the data in both contexts, structure-clone it (the default) โ€” just accept the copy cost.

Summary

  • Transferable Objects move data instead of copying it โ€” O(1) regardless of size
  • The transferred object becomes detached (unusable) in the sender
  • Works with postMessage, BroadcastChannel, and MessageChannel
  • Most useful for: ArrayBuffer (and typed arrays), ImageBitmap, OffscreenCanvas, MessagePort
  • Pass transferables as the second argument: postMessage(data, [transferable1, transferable2])
  • If you need the data in both contexts, don't transfer โ€” let it clone

Related Articles


ยฉ 2024, Built with Gatsby