demo · v130
main thread vs worker fan-out
The chromestatus motivation: "improve the performance of WebRTC applications, by offloading network send/receive operations from the main thread to a worker thread … combine an RTCDataChannel in workers with WebCodecs and OffscreenCanvas, to decode and render directly from a worker." This demo runs a synthetic high-rate data channel and compares main-thread vs dedicated-worker handling, with main-thread responsiveness as the load varies.
main-thread handling (legacy)
main thread
↕ (ondatachannel + onmessage)
RTCDataChannel
↕
app render + UI
every datagram payload is parsed/decompressed on the same thread that handles input + paint. UI hitches under load.
worker-transferred (Chrome 130+)
main thread
↓ postMessage(channel, [channel])
dedicated worker
↕
RTCDataChannel (now lives in worker)
↓ postMessage(decoded)
main thread — render only
channel transferred to the worker. Main thread only sees decoded frames, never raw datagrams.
800
main-thread main-loop FPS (simulated)
- datagrams processed
- 0
- main-thread fps
- – fps
- frame budget used
- 0%
worker fan-out (simulated)
- datagrams processed
- 0
- main-thread fps
- – fps
- main thread budget used
- 0%
the code
// Create the channel on the main thread as before.
const pc = new RTCPeerConnection(/* … */);
const channel = pc.createDataChannel("telemetry");
// Spin up a dedicated worker to handle this channel.
const worker = new Worker("worker.js", { type: "module" });
// New in Chrome 130: RTCDataChannel is transferable to dedicated workers.
channel.addEventListener("open", () => {
worker.postMessage({ kind: "channel", channel }, [channel]);
// From here on, channel is closed on the main thread —
// worker.js owns it.
});
// worker.js
self.onmessage = (e) => {
if (e.data.kind === "channel") {
const ch = e.data.channel;
ch.onmessage = (ev) => {
// decode in the worker. Combine with WebCodecs decoder +
// OffscreenCanvas if you also want to render here.
const decoded = decode(ev.data);
// forward minimal data to main for UI only
postMessage({ kind: "frame", decoded });
};
}
};