MessageChannel is a browser API that creates a new bidirectional communication channel with two connected ports. Messages sent via this API are processed as macro tasks in the browser's event loop.
Every MessageChannel instance exposes two read-only MessagePort properties:
port1: The first end of the channel, bound too the originating execution contextport2: The second end of the channel, typically transferred to a separate target context
Either port can send messages to the opposite end via postMessage(), and receive incoming messages by listening for the message event.
Basic usage example:
const channel = new MessageChannel();
const portA = channel.port1;
const portB = channel.port2;
portA.onmessage = (event) => {
console.log(`Port A received: ${event.data}`);
portA.postMessage('Hello Port B');
};
portB.onmessage = (event) => {
console.log(`Port B received: ${event.data}`);
};
portB.postMessage('Hello Port A');
You can permanently terminate the channel connection by calling close() on a port. Once closed, no further messages can be sent or received through that port.
Example of closing a channel:
const channel = new MessageChannel();
const portA = channel.port1;
const portB = channel.port2;
portA.onmessage = (event) => {
console.log(`Port A received: ${event.data}`);
portA.postMessage('Hello Port B');
// Terminate the connection
portA.close();
};
portB.onmessage = (event) => {
console.log(`Port B received: ${event.data}`);
portB.postMessage('What are you working on?');
};
portB.postMessage('Hello Port A');
In the example above, the follow-up message sent from portB after the first handshake will never be received by portA, since the connection was already closed.
Common Use Cases For MessageChannel
Cross-Context Communication
iframe Communication
MessageChannel is commonly used for communication between a parent page and an embedded iframe. After an initial handshake to transfer one port to the iframe, all subsequent communication can happen directly through the connected ports.
The typical workflow is:
- The parent page creates the channel, waits for the iframe to finish loading, then transfers the second port to the iframe via an initial
postMessage - The iframe receives the initial message, claims ownership of the transferred port, and both sides can communicate directly
Parent page example:
<!-- Parent page -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Parent Demo</title>
</head>
<body>
<h1>Parent Page</h1>
<input id="messageInput" type="text">
<div id="output">Parent output: </div>
<iframe id="embeddedFrame" src="./iframe-content.html"></iframe>
<script>
const channel = new MessageChannel();
const parentPort = channel.port1;
const iframePort = channel.port2;
const input = document.getElementById('messageInput');
const output = document.getElementById('output');
const iframe = document.getElementById('embeddedFrame');
input.addEventListener('input', (e) => {
parentPort.postMessage(e.target.value);
});
parentPort.onmessage = (e) => {
output.textContent = `Parent output: ${e.data}`;
};
iframe.addEventListener('load', () => {
iframe.contentWindow.postMessage('init-from-parent', '*', [iframePort]);
});
</script>
</body>
</html>
Iframe content example:
<!-- iframe-content.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Iframe Content</title>
</head>
<body>
<h1>Embedded Iframe</h1>
<div id="output">Iframe output: </div>
<script>
const output = document.getElementById('output');
let activePort = null;
window.addEventListener('message', (e) => {
activePort = e.ports[0];
activePort.postMessage('connected from iframe');
activePort.onmessage = (event) => {
output.textContent = `Iframe output: ${event.data}`;
};
});
</script>
</body>
</html>
Web Worker Communication
MessageChannel can also be used for comunication between the main thread and Web Workers, or between seperate Worker threads. The workflow is identical to iframe communication: you just transfer each port to the target Worker context after creating the channel.
Example:
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Worker Demo</title>
</head>
<body>
<script>
const channel = new MessageChannel();
const workerAPort = channel.port1;
const workerBPort = channel.port2;
const workerA = new Worker('worker-a.js');
const workerB = new Worker('worker-b.js');
workerA.postMessage('init', [workerAPort]);
workerB.postMessage('init', [workerBPort]);
</script>
</body>
</html>
// worker-a.js
self.onmessage = (e) => {
const port = e.ports[0];
port.onmessage = (event) => {
console.log(`Worker A received message: ${event.data}`);
};
};
// worker-b.js
self.onmessage = (e) => {
const port = e.ports[0];
port.postMessage('Greeting from Worker B');
};
Note: Browser security policies block Web Worker access from local file URLs, so run this demo on a local web server to avoid errors.
Asynchronous Deep Cloning
Since all messages sent through MessageChannel are automatically structurally cloned, you can leverage this behavior to implement an asynchronous deep copy of a JavaScript object:
const sourceObj = {
id: 1,
nested: {
foo: 3,
bar: 4
},
createdAt: new Date(),
missingKey: undefined
};
// Functions and Symbols cannot be cloned, and will throw errors
// myFunc: () => console.log('test'),
// symId: Symbol('uid')
const deepClone = (input) => {
return new Promise((resolve, reject) => {
const channel = new MessageChannel();
const receivePort = channel.port2;
const sendPort = channel.port1;
receivePort.onmessage = (e) => resolve(e.data);
receivePort.onmessageerror = (err) => reject(err);
sendPort.postMessage(input);
});
};
deepClone(sourceObj)
.then(clonedResult => {
console.log(clonedResult);
console.log(clonedResult === sourceObj); // false
});
This method has two important limitations:
- Special types including functions and Symbols are not supported by the structured clone algorithm, so cloning objects containing these values will throw an error
- The cloning process is inherently asynchronous, so it cannot be used for synchronous use cases and is generally slower than synchronous deep cloning implementations.
MessageChannel Usage In React Source Code
React 16+ introduced asynchronous rendering with time slicing, implemented via the Scheduler module that handles priority-based task scheduling. The Scheduler relies on MessageChannel to schedule rendering tasks, so that large rendering work can be split into chunks and paused to let the browser handle higher priority user input.
The simplified core scheduling logic from React's Scheduler source is:
let scheduleWorkUntilDeadline;
if (typeof setImmediate === 'function') {
scheduleWorkUntilDeadline = () => {
setImmediate(runWorkUntilDeadline);
};
} else if (typeof MessageChannel !== 'undefined') {
const workChannel = new MessageChannel();
const sendPort = workChannel.port2;
workChannel.port1.onmessage = runWorkUntilDeadline;
scheduleWorkUntilDeadline = () => {
sendPort.postMessage(null);
};
} else {
scheduleWorkUntilDeadline = () => {
setTimeout(runWorkUntilDeadline, 0);
};
}
React uses a fallback order of setImmediate → MessageChannel → setTimeout for task scheduling. All three are macro task APIs, and this order is chosen based on scheduling consistency: MessageChannel provides a more reliable and shorter delay than setTimeout(..., 0) in most browser environments.