Transferable Objects in JavaScript: Zero-Copy postMessage

March 22, 2026

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:

| Type | Use case | |---|---| | ArrayBuffer | Raw binary data, typed arrays | | MessagePort | MessageChannel endpoints | | ImageBitmap | Decoded images | | OffscreenCanvas | Canvas rendering off the main thread | | ReadableStream | Streaming data | | WritableStream | Streaming output | | TransformStream | Stream 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

| Scenario | Use | |---|---| | Small objects, strings, numbers | Structured clone (default) — fine | | Large binary data (images, audio, video frames) | Transfer ArrayBuffer | | Passing a canvas to a worker | Transfer OffscreenCanvas | | Setting up a direct channel between two workers | Transfer MessagePort | | Streaming data | Transfer 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