gpucanvas: 5,000 Live React Screens on a WebGPU Infinite Canvas

gpucanvas puts thousands of live DOM and React nodes on an infinite canvas drawn by WebGPU. 5,000 screens run at 113 fps where plain DOM manages 18. How it works, with HTML-in-Canvas in Chrome 157.

gpucanvas is an open-source TypeScript library that puts thousands of live DOM nodes on an infinite canvas. The GPU draws them. An editor selects, moves and resizes them. WGSL shaders and transitions run on them. An AI agent can read them and point at them.

The nodes stay real DOM. React state, events, CSS and accessibility work as usual. Click a screen and type into it: the click and the keystrokes go to the real element.

The same session on both sides: 5,000 live React screens, the same camera path, a message typed and sent. Left, plain DOM under a CSS transform. Right, gpucanvas. Chrome on a MacBook Pro, frame rates measured live.

In the 30-second benchmark tour at 5,000 screens, gpucanvas runs at 113 to 115 fps and plain DOM at 18 to 20 fps. In a longer tour at 10,000 screens, it is 108 fps against 9.

You can try the live demo in Chrome 157 or later, or install it:

npm install gpucanvas
import { GpuCanvas, CanvasNode } from "gpucanvas/react"; <GpuCanvas style={{ width: "100vw", height: "100vh" }}> {cards.map((card) => ( <CanvasNode key={card.id} x={card.x} y={card.y} width={240} height={150}> <Card {...card} /> </CanvasNode> ))} </GpuCanvas>;

Why a DOM infinite canvas slows down

The usual way to build an infinite canvas with real HTML on it is one container with a CSS transform for the camera. Pan or zoom, and the browser repaints and composites every visible element in that container. That is fine for a hundred cards. With a few thousand full app screens, each one a tree of real elements, the frame budget is gone. That is the left side of the video above.

The other usual way is to draw everything yourself in a <canvas>. That is fast, but you lose what the DOM gives you for free: text layout, inputs, focus, CSS, React components, accessibility. You end up rebuilding a browser inside the browser.

gpucanvas takes both: the elements stay in the DOM, and the GPU draws their pixels.

HTML-in-Canvas: the browser primitive that makes it work

Chrome 157 implements HTML-in-Canvas, a WICG proposal that lets a <canvas> hold live DOM children and draw them. Two pieces matter here:

  • <canvas content="drawable"> keeps its children as real, laid-out, interactive elements.
  • GPUQueue.drawElementImageToTexture() rasterizes one of those elements straight into a WebGPU texture.

So the DOM stays the source of truth, and WebGPU gets a texture of each element to draw however it wants. It is behind a flag for now: open chrome://flags/#canvas-draw-element, set it to Enabled and relaunch. Without it, gpucanvas falls back to a CSS-transformed container with the same API, and the demo runs 1,000 screens and says so.

How gpucanvas draws 5,000 screens in one call

Calling drawElementImageToTexture once per element per frame would be far too slow. Each call allocates and fully rasterizes, and Chrome manages about 250 to 550 of them per second in total. Most of gpucanvas is about calling it as rarely as possible.

  1. Pages. Nodes are packed side by side into pages of 2048 CSS pixels inside the canvas. Chrome records a page as one snapshot, so one call rasterizes dozens of nodes at once.
  2. Rasterize, then park. A page with nodes waiting for pixels becomes drawable. In the next paint event it is drawn into a texture-array atlas, whole, or one node at a time when only a few changed. Then it is parked with content-visibility: hidden, which keeps layout but skips paint. The browser spends nothing on it per frame.
  3. Thumbnails first. Every node gets a 1/16 scale thumbnail in an atlas layer that is never evicted, so a zoomed-out view of thousands of screens is always filled in. Sharper snapshots go on top at power-of-two scales, so zooming reuses them.
  4. Tiles up close. Past 100% zoom on a 2x display, only the visible part of a node is rasterized, as sharp tiles, center first.
  5. One draw call. Each frame culls nodes against the view and draws every visible one with one instanced quad draw. Panning and zooming never touch the DOM. Raster work has a per-frame budget, so the camera stays smooth while pixels fill in.
  6. Live updates. A MutationObserver marks a node dirty when its content changes. Loading images, web fonts and CSS transitions trigger a redraw too.
  7. Input. Only the page under the pointer is registered for hit testing, and a CSS transform puts the node under the pointer exactly where the GPU drew it. Clicks, typing, selection and hover all reach the real element.

What Chrome 157 does differently from the explainer

Building this meant finding where Chrome's implementation differs from the WICG explainer. If you are building on HTML-in-Canvas, these will save you time:

  • Hit testing needs registration. A drawable is hit-tested only after canvas.updateElementGeometry(el, {}) registers it. The canvasTransform option does not move the hit area.
  • The hit area is the CSS box. Position it with el.style.transform = canvas.getElementTransform(el, drawMatrix), where the draw matrix maps the element's default-size snapshot (CSS px × DPR) to canvas pixels.
  • Newly drawable elements are not reported in changedElements. Their snapshot is available from the next paint event.
  • Cropping still rasterizes everything. A source rectangle rasterizes the whole element at the requested scale before cropping. That is why a zoomed-in node moves to a page of its own.
  • Recording cost scales with drawables. It grows with the number of drawables on every requestPaint. 10,000 always-drawable nodes run at 22 fps even when idle, which is why pages get parked.

More than drawing: an editor, shaders and an agent view

Once every node is a texture on the GPU, a lot comes cheap.

Marks from an agent, a wipe and a dissolve, the glass lens and shockwaves, all on live components.

Editor. bindEditor turns the canvas into a design-tool editor. Click to select, double-click to go a layer deeper, down to any element or React component. Move and resize with snapping guides. The GPU draws the selection outlines, 2 CSS pixels wide at any zoom.

Shader effects. Run WGSL on live components: a glass lens, frost, shockwaves, glitch, VHS, or your own. An effect is a function over the node's pixels:

import { defineEffect } from "gpucanvas"; const scanlines = defineEffect({ name: "scanlines", animated: true, params: [3], wgsl: /* wgsl */ ` fn effect(input: EffectInput) -> vec4f { let color = source(input.uv); let y = input.uv.y * input.size.y + input.time * 20.0; return color * (0.85 + 0.15 * step(0.5, fract(y / input.params.x))); }`, }); canvas.setEffect(node, scanlines);

Effects draw from each node's snapshot in the same pass. With an effect on every one of the 5,000 screens, most cost nothing measurable. Glass, the heaviest, costs about 12%.

Snapshots. Every node is already on the GPU, so its pixels are one call away, with no headless browser: canvas.snapshot(node) returns ImageData, diffImages shows what changed between two versions, and recordFilmstrip keeps a history.

Agent view. An AI agent reads the canvas the way a person sees it. createAgentView(canvas).describe() returns what is in view as a short JSON tree with ids, roles, text and positions. A sign-in screen is about 35 entries and 3.5 KB, read in 3 ms. The agent can then capture any element or region as an image, and point at things with marks: arrows between elements, comment pins, notes and live cursors that follow the camera.

Transitions. Dissolve, wipe or fade one node out and another in on the GPU, while both stay live.

Benchmarks

Measured on an M-series Mac, Chrome 157, 1600 × 900 window, DPR 1. The tour is a 94-second session from the overview into clusters of app screens, close-ups up to 500% zoom, 20 clicks and typed messages, then back out.

Screensgpucanvas avg fpsgpucanvas p95DOM avg fpsDOM p95
5,0001179.2 ms2350.2 ms
10,00010816.7 ms9133.8 ms

DOM is the same screens in one CSS-transformed container, which is also gpucanvas's fallback. Press b in the demo to run the benchmark yourself.

Questions

Does gpucanvas work without WebGPU? Yes. Where WebGPU and HTML-in-Canvas are missing, it falls back to a CSS-transformed DOM container with the same API. Effects with a CSS look become CSS filters there.

Which browsers run the GPU path? Chrome 157 or later, with chrome://flags/#canvas-draw-element enabled.

Is it only for React? No. The core API takes plain DOM elements. gpucanvas/react adds <GpuCanvas>, <CanvasNode>, useEditor and a component picker.

Is it ready for production? It is early, version 0.4, and the API can still change. Two known gaps: Tab skips nodes on parked pages, so moving focus across pages needs a focus model, and compositing does not run in a worker yet.

What license? MIT.

Try it