Performance

ApexMaps' performance model is a rule enforced by CI, not a number that happens to hold on a fast laptop: a camera frame writes exactly one transform attribute regardless of feature count, and only marks whose pixel size carries meaning are rewritten per frame. SVG is the only renderer, and the whole performance path is free in every license tier; performance is not something this product charges for.

Why panning is constant-time

Features live in world space under a single group. Panning and zooming write one transform on that one element, so the per-frame cost in library code is constant in the feature count, and what scales with feature count is the browser rasterizing paths it already has.

The one exception is marks whose screen size or position carries a value: bubble radii and label positions are rewritten per frame, and that cost is O(marks), not O(features). A choropleth with 3,231 counties and no bubbles rewrites nothing extra per frame; a map with 40 bubbles on top of it rewrites 40 radii, not 3,231.

Measured numbers

Measured in headless Chromium with the frame rate unclamped, so intervals reflect work rather than the display's refresh rate. Parse is JSON.parse of the pack; Render is library work on already-parsed geometry, so neither includes the network. Reproject switches to a simple projection, rebuilding every path, which isolates path generation from projection math.

PackFeaturesParseRenderReprojectJS per frameFrame p50Frame p95
world/countries@110m1770.4 ms29 ms14 ms0.1 ms0.5 ms0.9 ms
eu/nuts3@20m1,5141.5 ms42 ms33 ms0.1 ms1.2 ms1.6 ms
us/counties@10m3,2313.0 ms331 ms87 ms0.1 ms2.4 ms3.1 ms
us/counties@10m + labels3,2312.5 ms312 ms83 ms0.0 ms2.5 ms3.3 ms

The budget is a p95 pan-and-zoom frame under 16 ms at 3,000 features. It comes in at 3.1 ms with 3,231 features, and 0.1 ms of that is library code: the rest is the browser rasterizing paths it already has.

Two things worth knowing from the table. The 331 ms initial render for US counties is mostly projection math: albersUsa, the pack's recommended projection, is a composite of three sub-projections with a point-in-region test per coordinate, roughly three times the cost of a simple projection, which is the price of insetting Alaska and Hawaii onto the same map as the mainland. And labels are culled by projected area before any per-label work runs, so 3,142 label candidates produce 69 placed labels without measuring 3,142 of them.

Why CI checks an invariant, not a millisecond

test/perf.test.ts asserts that a camera frame writes exactly one attribute regardless of feature count, and that feature path data is byte-identical before and after a pan. A wall-clock millisecond assertion would flake on a loaded CI runner, get skipped, and then nothing would be enforced, and a change that started reprojecting 3,000 features per frame would still pass a generous budget on a fast laptop while feeling wrong on every other machine.

Run it yourself

npm run examples   # builds, then serves examples/ on http://localhost:8084/examples/

Open bench.html to run the same measurement in your own browser, on your own machine, against world/countries@110m, eu/nuts3@20m, and us/counties@10m, with and without labels. It reports parse, render, reproject, JS-per-frame p95, frame p50/p95, the worst frame, and a dropped-frame count, and prints pass or fail against the 16 ms budget for each case.