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.