Skip to content

Rendering backends

d3gl renders the same projected-and-tessellated Scene through one of several backends. They are interchangeable: geometry is built once (backend-independent), and each backend turns it into pixels (or vector nodes) its own way. Pick one with the backend option on geoMap / plot (and the React <GeoMap> component):

import { geoMap } from "@mapequation/d3gl/map";
const map = geoMap(host, { width, height, projection, backend: "webgl" }); // the default

backend defaults to "webgl" when omitted.

BackendStartupBest forExport
"webgl" (default)One-time GPU device creation (can be 100s of ms cold)Large/dense scenes, smooth pan/zoom, the GPU globetoPNG() + toSVG()
"canvas"Instant (synchronous)Small/medium scenes, fastest first paint, no GPU dependencytoPNG() + toSVG()
"svg"Instant (synchronous)Vector output on screen, print, hand-editable DOMtoSVG()
"auto"Instant first paint, then upgradesThe best of canvas + webgl — see belowtoPNG() + toSVG()

All four share the same engine API (layer, recolor, enableZoom, setTransform, pick, setProjection, …); switching backend never changes how you drive the map.

WebGL is the best backend for an interactive map, but creating its GPU device has a fixed cost that delays the first paint — noticeable on a cold load, especially where the browser spins up a GPU process. Canvas2D has effectively zero startup but is slower for large or frequently-redrawn scenes.

backend: "auto" gives you both:

  1. It installs the Canvas2D backend synchronously and paints immediately.
  2. In the background it creates the WebGL device and, once ready, swaps to it transparently — preserving your layers, colors, view transform, and interaction.
  3. If WebGL is unavailable (or device creation fails), it stays on Canvas and logs a warning. The map keeps working.
const map = geoMap(host, { width, height, projection, backend: "auto" });
map.layer("ocean", [{ type: "Sphere" }], { fill: "#d4e6f5" });
map.layer("land", [world.land], { fill: "#e3e6ea" });
map.enableZoom([1, 8]);
// whenReady() resolves at the CANVAS first paint (early) — not when WebGL is ready.
await map.whenReady();
// The map is already visible and interactive here; the WebGL upgrade happens
// transparently a beat later.

The trade-off is a brief visual change when WebGL takes over (Canvas and WebGL anti-alias slightly differently). For most maps this is unnoticeable; if you need pixel-stable rendering from the first frame, choose "webgl" or "canvas" explicitly.

The canvas phase only pays for itself while it is cheaper than the WebGL device it is bridging. Content that a vector backend has to draw one shape at a time — a network() graph, a decluttered plot.points() layer — costs far more on Canvas2D than the whole upgrade does, so above ~10,000 elements the engine withholds it from the placeholder and lets the incoming WebGL backend paint the first frame instead. On a 611k-edge network that turns ~19 s of blocked main thread into ~0.2 s, with time-to-first-frame matching backend: "webgl".

The same threshold applies to geometry WebGL renders too — every geoMap layer, plot.layer(), every non-decluttered points() layer. That geometry is still built (it is shared, so skipping it would only move identical work later), but above ~10,000 drawables it is not pushed to the placeholder and not painted on it: the canvas frame would be thrown away by the WebGL install a moment later. Measured on a 120,000-polygon geoMap, that takes ~104 ms of main-thread work out of the layer() call; the cost scales linearly, so ~0.9 s at a million polygons.

You get a blank canvas for the (short) duration of the upgrade rather than a slow one — its box is reserved by your CSS, so nothing on the page moves when the real map arrives. Everything else is unchanged: whenReady() still resolves at the same moment, smaller scenes still paint instantly on canvas, and if WebGL turns out to be unavailable the engine falls back to canvas and draws the full detail there after all.

Call setBackend(...) to switch a live map — layers, colors, and the current zoom/pan are preserved across the swap:

map.setBackend("svg"); // e.g. to inspect or hand-edit the live vector DOM
map.setBackend("webgl");

Switching to the backend that is already live is a no-op (no re-render, no flicker). In particular, once "auto" has upgraded, the live backend is "webgl", so selecting "webgl" does nothing.

A network’s GPU force layout (layout({ backend: "gpu" })) runs on the WebGL backend’s device, so it moves with a swap: it frees its GPU memory before the old backend goes and continues where it was on the new one — on the GPU again for WebGL (including "auto"), on the CPU worker for Canvas and SVG. See Network.

A browser can take a page’s WebGL context away at any time, for example when its GPU process resets. The WebGL backend then stops drawing and builds nothing until the context is back, so nothing throws. When the browser gives the context back, the engine redraws on a fresh WebGL backend, as it does after a setBackend swap: the same layers, view and selection. A running GPU layout continues on the CPU worker. Changes made while the context is gone (new data, styles, the view) are kept: toSVG() exports them, and the redraw shows them. toPNG() throws while the context is gone, since there is no drawn image to copy.

If the context has not come back after 10 s, or it came back but building the new WebGL backend failed, the engine calls onContextUnrestored once, so you can tell the user, for example to reload the page, or switch to Canvas with setBackend("canvas"). setBackend("webgl") rebuilds at once without waiting.

const net = network(host, {
backend: "webgl",
onContextUnrestored: () => showMessage("The graphics card stopped responding. Reload to redraw."),
});

A context that comes back later still redraws.

The <GeoMap> component takes the same backend prop, including "auto":

import { GeoMap } from "@mapequation/d3gl/react";
<GeoMap width={720} height={380} projection={projection} backend="auto" onReady={(map) => { /* … */ }} />

toSVG() works on every backend and always serializes the current view — you do not need to switch to "svg" to get vector output. The raster backends (webgl, canvas, and an upgraded auto) additionally offer toPNG().

const svg = map.toSVG(); // vector, on any backend
const png = map.toPNG(); // raster, on webgl/canvas/auto

toPNG() is what the screen shows: the canvas as drawn, at its device pixels (the CSS size times devicePixelRatio, so a 650×560 view exports 1300×1120 on a retina screen), with the same antialiasing and colours on both raster backends. The labels the WebGL backend shows in its HTML overlay are painted on top.

On WebGL, content the GPU draws outside the retained scene — the instanced network glyphs/links, an LOD cut frontier, decluttered plot points — has no scene geometry to serialize, so the engine builds a vector view of the lanes’ current emit at export time and hands it to the backend alongside the retained layers. That happens only inside toSVG(), so it costs nothing while you pan and zoom.