Skip to content
gpu-components
Why GPU

Do not use the GPU merely because it is possible.

Every component in this library has to clear a six-question gate. Components that fail it are rejected — including ones that would look impressive in a screenshot.

The gate

Six questions, all of which must pass.

01

Does the workload exceed what one CPU frame can touch?

The ceiling is primitives issued per frame from JS: ~5k DOM nodes, ~50k Canvas2D fillRects at 60fps. Instancing changes the unit from one call per primitive to one call per million.

02

Is there per-element work that is data-parallel?

Colour mapping, thresholding, normalisation, projection, LOD bucketing, min/max reduction.

03

Does interaction re-derive the whole dataset?

Zoom, pan, brush and filter over an immutable dataset is the ideal case: upload once, re-render from a changed uniform. CPU pipelines re-walk the data every time.

04

Can we avoid CPU↔GPU round-trips?

Anything needing a synchronous readback per frame is disqualified. vgpu is explicit that read() is for tests and diagnostics, not a hot path.

05

Is the text budget bounded?

Text is the tax. More than ~1k glyph runs per frame means the glyph atlas has to exist first.

06

Does it produce reusable runtime primitives?

A component needing a bespoke pipeline nobody else reuses is a demo, not a library investment.

Rejected outright: particle backgrounds, shader wallpapers, decorative post-processing, generic 3D scene viewers. They fail question 1 (no data-scale problem) or question 6 (no primitive another application component needs). vgpu already ships vgpu/scene for 3D meshes — duplicating it is an explicit non-goal.
Measured in your browser

The ceiling, live.

Every span in the dataset is on screen, so every span is drawn every frame. This is the case a GPU instanced draw collapses into a single call.

Renderer
Frames / sec
p50 frame
p95 frame
Spans drawn
10,000
Division of work

What runs where, and what must not move.

GPU

Data-parallel, per-frame

  • Instanced primitive rendering from a storage buffer
  • Viewport transform — pan/zoom is a uniform write
  • Colour mapping, normalisation, thresholding
  • LOD and density binning with atomics
  • Reductions for auto-ranging and histograms
  • Selection as a bitset the shader branches on
  • ID-buffer picking where a CPU index is impractical
Worker

Once per dataset

  • Parsing — JSON, OTLP, Arrow
  • Building columnar typed arrays
  • Sorting by (track, start) — once, never per frame
  • Building the spatial index for CPU hit-testing
  • String interning
  • Transfer via Transferable, zero copy
Main thread

Stays on CPU, deliberately

  • All string handling — formatting, search, collation
  • Track and column layout
  • Hit-testing via the sorted index — O(log n), exact, immediate
  • Tooltip contents
  • The accessibility tree
Hit-testing on the CPU is a feature, not a fallback. Because the data is sorted with a per-track index, a hover test is a binary search: exact and available this frame. GPU picking is always one frame late, so it is reserved for layers where a cheap CPU index genuinely is not possible — dense scatter, graph nodes.
Honesty

Where this argument gets uncomfortable.

Three things that will be true in the published benchmarks, stated here before anyone finds them.

WebGL2 instancing is also fast

For raw quad throughput, WebGPU’s advantage over a competent WebGL2 instanced renderer may be modest. This was the biggest technical risk in the project. It was measured before any runtime code was written, and the comparison is published whether or not it flatters us. The durable WebGPU advantages are compute in the data path, indirect draws and dispatches, and storage-buffer-driven vertex work — not fill rate.

Canvas2D is better than people assume

At viewport scale, Canvas2D is sufficient — which is precisely why a canvas data grid already scrolls millions of rows at 60fps today. Anyone selling you a GPU grid on scroll performance is selling you something you already have.

Below the crossover, the GPU path is slower

Upload cost and pipeline overhead dominate at small N. The hypothesis is a crossover somewhere around 20k–50k primitives. The measured number goes in each component’s docs, in a section titled “When NOT to use this”, along with a recommendation for what to use instead.