GPU Rendering and Choosing a Renderer
The CPU draws points one after another. The GPU draws them all at once. Knowing when to make that switch is the real skill.
The story
A flight-tracking dashboard plots live aircraft positions on a map, plus the last 30 minutes of each track: around 250,000 points, updated every second. The first version draws them on a canvas 2D layer. It manages 8 frames per second while panning, and the fan on every laptop in the operations room spins up.
The team profiles it. Nothing is wasted: the code already batches by colour, culls off-screen points and uses one path per layer. It's simply a quarter of a million rect() calls described one by one on the CPU every frame.
They move the data layer to WebGL using deck.gl. Panning is now smooth at 60 fps, and CPU time per frame drops from over 100 ms to a few milliseconds, because the per-point work moved to the GPU.
The new problems are different in kind:
- Labels: drawing crisp text in WebGL is surprisingly hard. The callsigns move to an HTML overlay that only labels the 50 aircraft nearest the cursor.
- "WebGL context lost" on some laptops after they wake from sleep. The map goes blank until a reload.
- One team member's old office PC has its GPU blocklisted by the browser and falls back to software rendering, where WebGL is slower than the canvas version was.
The GPU is the most powerful renderer in the browser. It's also the one with the most moving parts. This chapter is about when the trade is worth it.
Part 1: What the GPU is good at
A CPU core runs one sequence of instructions very fast. A GPU runs the same small program on thousands of inputs at once. Drawing a million points is the perfect GPU job: the same calculation (where is this point on screen? what colour is this pixel?) repeated a million times, independently.
The rules for fast GPU rendering follow from that picture:
- Few draw calls, lots of data per call. A draw call has fixed overhead. One call with a million points is fast. A million calls with one point each is slower than canvas.
- Upload once, update little. Moving data from CPU memory to GPU memory costs time. Keep buffers on the GPU, and update only what changed (
bufferSubDataon a range). - Do per-point math in the shader. If points move along known paths, upload start position, velocity and start time once, and compute the current position in the vertex shader from a single
timevalue. The CPU's per-frame work drops to setting one number. - Draw many copies of one shape with instancing. A thousand identical icons with different positions and colours is one shape plus a per-instance array: one draw call (
drawArraysInstancedin WebGL2).
Part 2: WebGL, WebGL2 and WebGPU
| API | What it is | Status |
|---|---|---|
| WebGL | OpenGL ES 2.0 in the browser | Everywhere, including old devices |
| WebGL2 | OpenGL ES 3.0: instancing built in, more texture formats, better buffers | Supported in all current major browsers |
| WebGPU | Modern design (like Vulkan/Metal/Direct3D 12), with compute shaders for general GPU computation | Shipping in recent versions of the major browsers, with gaps on some platforms. Check support for your audience and keep a WebGL fallback. |
For data visualisation, WebGPU's big addition is compute: aggregation, binning, layout and filtering of millions of points can run on the GPU instead of in a worker. For most apps today, WebGL2 through a library is the safe default.
Part 3: What the GPU makes harder
| Problem | What to do |
|---|---|
| Text | Use signed-distance-field (SDF) font atlases (libraries handle this), or put labels in an HTML/canvas overlay and only label what matters |
| Context loss: the browser can take the GPU context away (driver reset, sleep, too many contexts) | Listen for webglcontextlost / webglcontextrestored, and be able to re-upload everything from your own data |
| Too many contexts: browsers cap active WebGL contexts per page (often around 16) | One shared context for a whole dashboard, not one per chart |
| Device variety: integrated GPUs, mobile GPUs, blocklisted drivers with software fallback | Test on low-end hardware. Detect slow frames and fall back to canvas or fewer points. |
| Startup cost: shader compilation, buffer uploads | Compile once, warm up during load, avoid recompiling on state changes |
| Debugging: no DevTools elements to inspect | Spector.js frame capture, chrome://gpu to check hardware acceleration |
| Accessibility and events | Same as canvas: your own hit testing (often GPU colour picking) and a DOM layer for a11y (Chapter 6) |
Part 4: The library landscape
You rarely write raw WebGL for product work. Pick by the job:
| Library | Good for |
|---|---|
| deck.gl | Large-scale geospatial and data layers (millions of points, arcs, hexbins), often on top of a base map |
| PixiJS | Fast 2D sprite rendering: games, interactive canvases, big diagrams |
| regl | A thin, functional layer over WebGL when you want control without the boilerplate |
| three.js | 3D scenes |
| uPlot | Very fast time-series charts on canvas 2D (no GPU needed for many use cases) |
Plotly scattergl / ECharts with large-data modes | Familiar chart APIs with GPU or optimised canvas paths for big series |
| Commercial (LightningChart, SciChart) | Extreme point counts, scientific and financial charting |
Figma is the well-known example of going all the way: its editor renders the design canvas with WebGL, driven by a C++ engine compiled to WebAssembly, which is how it keeps huge documents interactive in a browser.
Part 5: Race them on your device
Below, the same moving points are drawn four ways: DOM elements, SVG circles, Canvas 2D (batched into one path) and raw WebGL points. The frame rate and per-frame script time are measured live in this tab. Try all three sizes. Then try it on your phone.
Two things to notice:
- At small sizes, all four are fine. Choosing WebGL for a 500-point chart buys nothing and costs a lot.
- For DOM and SVG, the script time is low even when the frame rate collapses. The cost is in style, layout and paint after your script runs, which is invisible to a JavaScript timer and obvious in the Performance panel.
Part 6: Choosing a renderer
The thresholds below are ranges, not rules. They depend on the device, how often things move, and how complex each shape is. Measure on your weakest target device.
| Renderer | Comfortable range | You get | You pay |
|---|---|---|---|
| DOM | Hundreds of moving elements, more if static | Layout, text, forms, events, a11y, CSS | Per-element style/layout/paint cost |
| SVG | Up to a few thousand shapes | Vector crispness, CSS, per-shape events, a11y | Every shape is a DOM node |
| Canvas 2D | Thousands to roughly a hundred thousand per frame | One bitmap, full control, cheap redraws | Your own hit testing, a11y, text layout |
| WebGL / WebGPU | Hundreds of thousands to millions | GPU parallelism, near-zero CPU per frame | Text, context loss, device variety, complexity |
Most real apps are hybrids
Use the GPU for the dense data, and the DOM for everything a person reads or clicks. Each layer gets the renderer that fits it, which is the story's final architecture.
Numbers to remember
Ranges vary by device. Verify with the simulation above and a Performance recording.
| Number | Meaning |
|---|---|
| ~1,000s | Where SVG with per-frame updates typically starts to struggle |
| ~100k | Where batched canvas 2D typically becomes CPU-bound at 60 fps |
| Millions | What WebGL handles when data stays on the GPU |
| ~16 | A common per-page limit on active WebGL contexts |
| 1 | The ideal number of draw calls per layer |
The DevTools workflow
chrome://gpu: check that WebGL is hardware-accelerated, not a software fallback.- Performance panel: the GPU track shows GPU work. A long GPU block with little script means fragment work: too many large or overlapping shapes, or expensive shaders.
- Spector.js (browser extension) captures one WebGL frame and lists every draw call and state change. Count the draw calls.
- Test context loss:
canvas.getContext('webgl').getExtension('WEBGL_lose_context').loseContext()in the console, then restore, and check the app recovers. - Throttle with a low-end device or CPU slowdown. The GPU path is fast on your laptop. Check the users' hardware.
Cheat sheet
| Situation | Choose |
|---|---|
| A normal chart, hundreds of points | SVG (or any chart library) |
| Interactive diagram, a few thousand shapes | SVG, or canvas if it animates heavily |
| Tens of thousands of points, live updates | Canvas 2D, batched and culled |
| Canvas profile is CPU-bound at 100k+ | WebGL via deck.gl / PixiJS / regl |
| Points move on predictable paths | Animate in the vertex shader from a time uniform |
| Many identical icons | Instancing |
| Many charts on one page | One shared GL context (or canvas), not one per chart |
| Labels on a GPU layer | SDF text, or an HTML overlay for the few labels that matter |
| Unreliable GPUs in your audience | Detect slow frames, fall back to canvas or fewer points |
Say it in the interview
"I'd pick the renderer by element count and update rate: SVG up to a few thousand shapes because it's cheap to build and accessible, canvas 2D for tens of thousands, and WebGL beyond roughly a hundred thousand or when profiling shows canvas is CPU-bound. Here, 250k moving points means a WebGL data layer through deck.gl, with data in GPU buffers updated incrementally and labels in an HTML overlay for the nearest few aircraft. I'd handle context loss by re-uploading from our own store, and fall back to fewer points on devices with software rendering."
Go deeper
- Canvas 2D Engineering: layers, culling and hit testing, which all apply to GPU layers too.
- Real-Time Data Dashboards: downsampling before you draw, whatever the renderer.
- Off the Main Thread: OffscreenCanvas works with WebGL contexts too.
- Next: Design Drills.