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); // 0On 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 tabOne 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 goneIf 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, andMessageChannel - 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