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.

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

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.