Chromium's OffscreenCanvas Implementation for WebGL Workers

This article explores the architectural mechanisms Chromium uses to implement OffscreenCanvas, enabling high-performance WebGL rendering inside a Web Worker. By decoupling the canvas from the Document Object Model (DOM), establishing direct inter-process communication (IPC) between the worker thread and the GPU process, and utilizing independent compositor frame sinks, Chromium allows complex graphics to render seamlessly without blocking the main browser thread.

Decoupling the Canvas from the DOM

Standard <canvas> elements are strictly tied to the main thread because they are part of the DOM. To break this dependency, Chromium implements the transferControlToOffscreen() API. When this method is called on an HTMLCanvasElement, Chromium strips the rendering context capabilities from the DOM element, turning it into a lightweight placeholder.

During this transfer, the main thread generates a unique identifier known as a SurfaceId and assigns it to the placeholder canvas. The newly created OffscreenCanvas object—which holds a reference to this SurfaceId—is then serialized and transferred to the Web Worker thread.

Thread-Local GPU Command Buffers

Once the OffscreenCanvas arrives in the Web Worker, the worker can initialize a WebGLRenderingContext on it. Chromium’s rendering engine, Blink, handles this by allocating a dedicated GPU Command Buffer directly on the worker thread.

In Chromium's multi-process architecture, renderer processes cannot access the hardware GPU directly for security reasons. Instead, when the Web Worker executes WebGL commands (such as gl.drawArrays() or gl.bindTexture()), Blink serializes these calls into a shared memory ring buffer. Because the worker has its own command buffer, it does not need to proxy these WebGL commands back to the main thread.

Direct Communication via Mojo IPC

To execute the serialized WebGL commands, the Web Worker communicates directly with Chromium's GPU process using Mojo, Chromium's inter-process communication (IPC) system.

The worker thread sends an IPC message telling the GPU process to read from the shared memory command buffer. The GPU process parses the commands, validates them for security, translates them into native graphics API calls (via ANGLE, which translates WebGL to DirectX, Metal, or Vulkan), and executes them on the physical GPU. The resulting rendered image is stored in a hardware texture within the GPU's memory, known as a SharedImage.

Autonomous Frame Submission to the Viz Compositor

Rendering the WebGL scene into a GPU texture is only half the process; the browser must still display that texture at the correct location on the webpage. Chromium achieves this through its display compositor, known as Viz, which runs inside the GPU process.

Rather than sending the rendered texture back to the main thread to be composited with the rest of the DOM, Chromium makes the Web Worker an autonomous compositor client. The OffscreenCanvas inside the worker establishes its own CompositorFrameSink—a direct Mojo IPC pipe to the Viz compositor.

Surface Synchronization and Display

The Web Worker runs its own requestAnimationFrame loop, completely independent of the main thread's frame rate. At the end of a rendering tick in the worker, the OffscreenCanvas packages the reference to the rendered GPU texture (the SharedImage) into a data structure called a CompositorFrame.

The worker submits this CompositorFrame directly to the Viz compositor via its CompositorFrameSink, tagging it with the SurfaceId that was generated when the canvas was first transferred.

Meanwhile, the main thread submits its own CompositorFrame containing the webpage's DOM layout, which includes the placeholder <canvas> and its matching SurfaceId. Using a mechanism called Surface Synchronization, the Viz compositor locates the matching SurfaceIds and stitches the worker's WebGL texture directly into the visual hole left by the main thread's placeholder canvas. Because this stitching happens entirely inside the GPU process, the Web Worker can render and display WebGL frames at 60fps or higher, even if the main thread is completely frozen by heavy JavaScript execution.