Low-Level Web Rendering
Choose the least specialized renderer that meets the product need. A GPU path is not automatically faster, and “painting without the DOM” is usually the wrong model for text, controls, or accessible application UI.
Production decision router
| Need |
Default |
Why / boundary |
| Text, forms, navigation, selectable content |
DOM + CSS |
Native semantics, layout, accessibility, input, search, and internationalization. |
| Scalable diagrams with addressable elements |
SVG |
DOM semantics plus vector rendering; watch very large node counts. |
| Dense custom 2D pixels, drawing, charts, sprites |
Canvas 2D |
Immediate-mode rendering; supply a semantic DOM alternative for interaction. |
| Canvas work blocks input/scroll |
OffscreenCanvas + Worker |
Moves supported rendering and preparation off the main thread. Feature-detect. |
| Portable 3D/shaders |
WebGL2 |
Mature compatibility; use an engine unless raw API control is the product. |
| Compute, storage buffers, explicit modern GPU pipelines |
WebGPU |
Progressive enhancement with a WebGL/CPU fallback; see webgpu. |
| vgpu-based shader effects, procedural visuals, or custom 3D |
Vercel vgpu over WebGPU |
Use the installed version's docs; see vgpu. Preserve the surrounding semantic UI. |
| CPU-heavy parsing, simulation, codecs |
Worker, then WASM if measured |
WASM accelerates compute; it does not replace the DOM or choose the renderer. |
| Video frame decode/encode or transforms |
WebCodecs when supported |
Specialized media frames; retain <video> / server paths where compatibility matters. |
Apply the table in this order:
- Preserve semantics first. If users read, edit, focus, select, search, translate, or automate it, keep a server-rendered or pre-rendered real DOM representation.
- Measure the bottleneck: DOM count/layout, paint, main-thread JS, draw calls, fill rate, GPU time, transfers, or memory.
- Change only the constrained layer. Moving pixels to a canvas does not fix unrelated data or scripting work.
- Define fallback and recovery before adopting a less portable API.
Browser frame pipeline
Each frame may perform:
JavaScript → style → layout → paint → composite
- Batch geometry reads before writes; reading layout after a mutation can force synchronous layout.
- Prefer transforms and opacity for motion when they avoid layout/paint, but verify in DevTools—compositing is an implementation decision, not a promise.
- Use
will-change only around a measured transition; permanent layers consume memory.
- Reduce DOM/layout scope before replacing accessible markup with custom pixels (
contain, content-visibility, virtualization, smaller subtrees).
Canvas 2D baseline
Size the backing store from actual display pixels and update it when the element changes:
function resizeCanvas(canvas, ctx) {
const rect = canvas.getBoundingClientRect();
const dpr = Math.min(window.devicePixelRatio || 1, 2);
const width = Math.max(1, Math.round(rect.width * dpr));
const height = Math.max(1, Math.round(rect.height * dpr));
if (canvas.width === width && canvas.height === height) return;
canvas.width = width;
canvas.height = height;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
- Clamp DPR when fill rate, VRAM, memory, or power matters; expose quality as a product decision.
- Drive animation with
requestAnimationFrame; use its timestamp and stop when nothing changes.
- Reuse paths, images, typed arrays, and state. Avoid allocations, text measurement, filters, and readbacks in hot loops.
- Layer static and dynamic content only when it reduces measured redraw cost.
- Use dirty rectangles only when tracking/overdraw complexity beats a full redraw in measurements.
OffscreenCanvas and workers
if ('transferControlToOffscreen' in canvas) {
const offscreen = canvas.transferControlToOffscreen();
worker.postMessage({ canvas: offscreen }, [offscreen]);
} else {
startMainThreadRenderer(canvas);
}
- Keep input, accessibility, and DOM state on the main thread; send compact state snapshots or commands.
- Transfer ownership once. Do not copy large frame payloads when transferable buffers or shared state are justified.
- Worker support differs by context/API. Test the exact browser matrix; the fallback is part of the architecture.
HTML-in-Canvas and CSS Paint: lab only
HTML-in-Canvas is a WICG proposal, not a production rendering dependency. The current explainer keeps real DOM descendants under <canvas layoutsubtree>, marks drawn elements with drawable, and uses drawElementImage(), texElementSubImage2D(), and GPUQueue.drawElementImageToTexture() for Canvas 2D, WebGL, and WebGPU. Earlier drafts used different names. It is not DOM-free painting, and vgpu does not require it.
Current status reviewed 9 September 2026:
- Chromium records the origin trial as M148–M150, subsequently extended through M154.
- Gecko and WebKit have no positive implementation signal.
- The API shape, privacy rules, hit testing, and accessibility behavior are still being developed.
- The explainer now includes
CanvasPaintEvent.changedElements, transferable ElementImage snapshots for workers, and explicit element geometry updates. Snapshot drawing and DOM updates are distinct: mutations during paint appear in the next rendering update.
- WebGL/WebGPU experiments must update element geometry for hit testing and accessibility; 2D drawing can update it automatically. Captured snapshots do not make DOM layout or input independent of the main thread.
- Cross-origin embedded content and readback are restricted; consult the current privacy rules rather than treating this as unrestricted DOM screenshot access.
Rules:
- Use it only for a lab/prototype or a separately guarded enhancement.
- Feature-detect the exact method; never infer it from a Chrome version.
- Keep an equivalent semantic DOM path visible and meaningful when the experimental drawing method is absent; descendants used only as ordinary canvas fallback content are not enough in a canvas-capable browser.
- Production fallback: DOM overlay for interactive UI; DOM/SVG/Canvas 2D for 2D content; DOM overlay or established texture pipeline for WebGL/WebGPU scenes.
- Never make an origin-trial token or browser flag a normal-user requirement.
CSS Paint worklets are also a specialized progressive enhancement, not an escape hatch from DOM/CSS architecture. Keep an ordinary CSS background/border fallback and do not put content or essential state only in paint output.
Accessibility and input
- Canvas pixels do not create semantics. Provide DOM controls, names, focus order, keyboard behavior, announcements, and equivalent text/data.
- Keep hit testing in one coordinate system and account for CSS size, backing-store size, camera transforms, zoom, and DPR.
- Preserve no-JS content, reduced motion, forced colors, text scaling, and high contrast outside custom rendering where possible.
- Do not duplicate an interactive control in both canvas and DOM accessibility trees.
Measure before escalating
- Record a representative interaction in the Performance panel; identify scripting, layout, paint, raster, or GPU work.
- Count DOM nodes, draw calls, submissions, buffer/texture uploads, and frame allocations.
- Set explicit pixel, texture/VRAM, frame-time, and power budgets before increasing visual complexity.
- Test integrated GPUs, mobile thermal throttling, high-DPR displays, and background/hidden tabs.
- Compare against the simpler renderer. Keep the specialized path only when the user-visible win survives target-device testing.
Ship gate
Reference
1---2name: low-level-web-rendering3description: Chooses between DOM, SVG, Canvas, WebGL, WebGPU, and worker rendering, or evaluates experimental HTML-in-Canvas. Use for renderer selection or architecture tradeoffs, not routine work in an already-selected GPU library.4license: MIT5---67# Low-Level Web Rendering89Choose the least specialized renderer that meets the product need. A GPU path is not automatically faster, and “painting without the DOM” is usually the wrong model for text, controls, or accessible application UI.1011## Production decision router1213| Need | Default | Why / boundary |14|---|---|---|15| Text, forms, navigation, selectable content | DOM + CSS | Native semantics, layout, accessibility, input, search, and internationalization. |16| Scalable diagrams with addressable elements | SVG | DOM semantics plus vector rendering; watch very large node counts. |17| Dense custom 2D pixels, drawing, charts, sprites | Canvas 2D | Immediate-mode rendering; supply a semantic DOM alternative for interaction. |18| Canvas work blocks input/scroll | OffscreenCanvas + Worker | Moves supported rendering and preparation off the main thread. Feature-detect. |19| Portable 3D/shaders | WebGL2 | Mature compatibility; use an engine unless raw API control is the product. |20| Compute, storage buffers, explicit modern GPU pipelines | WebGPU | Progressive enhancement with a WebGL/CPU fallback; see `webgpu`. |21| vgpu-based shader effects, procedural visuals, or custom 3D | Vercel vgpu over WebGPU | Use the installed version's docs; see `vgpu`. Preserve the surrounding semantic UI. |22| CPU-heavy parsing, simulation, codecs | Worker, then WASM if measured | WASM accelerates compute; it does not replace the DOM or choose the renderer. |23| Video frame decode/encode or transforms | WebCodecs when supported | Specialized media frames; retain `<video>` / server paths where compatibility matters. |2425Apply the table in this order:26271. Preserve semantics first. If users read, edit, focus, select, search, translate, or automate it, keep a server-rendered or pre-rendered real DOM representation.282. Measure the bottleneck: DOM count/layout, paint, main-thread JS, draw calls, fill rate, GPU time, transfers, or memory.293. Change only the constrained layer. Moving pixels to a canvas does not fix unrelated data or scripting work.304. Define fallback and recovery before adopting a less portable API.3132## Browser frame pipeline3334Each frame may perform:3536`JavaScript → style → layout → paint → composite`3738- Batch geometry reads before writes; reading layout after a mutation can force synchronous layout.39- Prefer transforms and opacity for motion when they avoid layout/paint, but verify in DevTools—compositing is an implementation decision, not a promise.40- Use `will-change` only around a measured transition; permanent layers consume memory.41- Reduce DOM/layout scope before replacing accessible markup with custom pixels (`contain`, `content-visibility`, virtualization, smaller subtrees).4243## Canvas 2D baseline4445Size the backing store from actual display pixels and update it when the element changes:4647```js48function resizeCanvas(canvas, ctx) {49 const rect = canvas.getBoundingClientRect();50 const dpr = Math.min(window.devicePixelRatio || 1, 2);51 const width = Math.max(1, Math.round(rect.width * dpr));52 const height = Math.max(1, Math.round(rect.height * dpr));53 if (canvas.width === width && canvas.height === height) return;54 canvas.width = width;55 canvas.height = height;56 ctx.setTransform(dpr, 0, 0, dpr, 0, 0);57}58```5960- Clamp DPR when fill rate, VRAM, memory, or power matters; expose quality as a product decision.61- Drive animation with `requestAnimationFrame`; use its timestamp and stop when nothing changes.62- Reuse paths, images, typed arrays, and state. Avoid allocations, text measurement, filters, and readbacks in hot loops.63- Layer static and dynamic content only when it reduces measured redraw cost.64- Use dirty rectangles only when tracking/overdraw complexity beats a full redraw in measurements.6566## OffscreenCanvas and workers6768```js69if ('transferControlToOffscreen' in canvas) {70 const offscreen = canvas.transferControlToOffscreen();71 worker.postMessage({ canvas: offscreen }, [offscreen]);72} else {73 startMainThreadRenderer(canvas);74}75```7677- Keep input, accessibility, and DOM state on the main thread; send compact state snapshots or commands.78- Transfer ownership once. Do not copy large frame payloads when transferable buffers or shared state are justified.79- Worker support differs by context/API. Test the exact browser matrix; the fallback is part of the architecture.8081## HTML-in-Canvas and CSS Paint: lab only8283HTML-in-Canvas is a **WICG proposal**, not a production rendering dependency. The current explainer keeps real DOM descendants under `<canvas layoutsubtree>`, marks drawn elements with `drawable`, and uses `drawElementImage()`, `texElementSubImage2D()`, and `GPUQueue.drawElementImageToTexture()` for Canvas 2D, WebGL, and WebGPU. Earlier drafts used different names. It is not DOM-free painting, and vgpu does not require it.8485Current status reviewed 9 September 2026:8687- Chromium records the origin trial as M148–M150, subsequently extended through M154.88- Gecko and WebKit have no positive implementation signal.89- The API shape, privacy rules, hit testing, and accessibility behavior are still being developed.90- The explainer now includes `CanvasPaintEvent.changedElements`, transferable `ElementImage` snapshots for workers, and explicit element geometry updates. Snapshot drawing and DOM updates are distinct: mutations during `paint` appear in the next rendering update.91- WebGL/WebGPU experiments must update element geometry for hit testing and accessibility; 2D drawing can update it automatically. Captured snapshots do not make DOM layout or input independent of the main thread.92- Cross-origin embedded content and readback are restricted; consult the current privacy rules rather than treating this as unrestricted DOM screenshot access.9394Rules:9596- Use it only for a lab/prototype or a separately guarded enhancement.97- Feature-detect the exact method; never infer it from a Chrome version.98- Keep an equivalent semantic DOM path visible and meaningful when the experimental drawing method is absent; descendants used only as ordinary canvas fallback content are not enough in a canvas-capable browser.99- Production fallback: DOM overlay for interactive UI; DOM/SVG/Canvas 2D for 2D content; DOM overlay or established texture pipeline for WebGL/WebGPU scenes.100- Never make an origin-trial token or browser flag a normal-user requirement.101102CSS Paint worklets are also a specialized progressive enhancement, not an escape hatch from DOM/CSS architecture. Keep an ordinary CSS background/border fallback and do not put content or essential state only in paint output.103104## Accessibility and input105106- Canvas pixels do not create semantics. Provide DOM controls, names, focus order, keyboard behavior, announcements, and equivalent text/data.107- Keep hit testing in one coordinate system and account for CSS size, backing-store size, camera transforms, zoom, and DPR.108- Preserve no-JS content, reduced motion, forced colors, text scaling, and high contrast outside custom rendering where possible.109- Do not duplicate an interactive control in both canvas and DOM accessibility trees.110111## Measure before escalating112113- Record a representative interaction in the Performance panel; identify scripting, layout, paint, raster, or GPU work.114- Count DOM nodes, draw calls, submissions, buffer/texture uploads, and frame allocations.115- Set explicit pixel, texture/VRAM, frame-time, and power budgets before increasing visual complexity.116- Test integrated GPUs, mobile thermal throttling, high-DPR displays, and background/hidden tabs.117- Compare against the simpler renderer. Keep the specialized path only when the user-visible win survives target-device testing.118119## Ship gate120121- [ ] The renderer matches the content’s semantic and interaction needs.122- [ ] The bottleneck was measured before the architecture changed.123- [ ] DPR, resize, visibility, context/device loss, and cleanup are handled.124- [ ] Unsupported browsers and no-JS users get a usable path, not an empty canvas.125- [ ] The main thread remains responsive and accessibility/reduced-motion behavior is verified.126- [ ] Experimental APIs are isolated and removable.127128## Reference129130- WHATWG: [HTML canvas element](https://html.spec.whatwg.org/multipage/canvas.html).131- W3C: [WebCodecs](https://www.w3.org/TR/webcodecs/) and [CSS Painting API](https://drafts.css-houdini.org/css-paint-api/).132- MDN: [Canvas API](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API), [OffscreenCanvas](https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas), and [WebCodecs](https://developer.mozilla.org/en-US/docs/Web/API/WebCodecs_API).133- WICG: [HTML-in-Canvas explainer](https://github.com/WICG/html-in-canvas).134- Chromium: [HTML-in-Canvas intent](https://groups.google.com/a/chromium.org/g/blink-dev/c/t_nGEmJ_v4s) and [experiment extension](https://groups.google.com/a/chromium.org/g/blink-dev/c/BpWbzJ9P22s).135- web.dev: rendering performance and avoiding large, complex layouts and layout thrashing.