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 });
    };
  }
};

see also