[EPIC PERFORMANCE] Low Performant Maps #788
Replies: 2 comments
A proposal to step back and rethink the map, empiricallyFirst — thank you for the diagnosis above. The GPU-layer / re-rasterisation analysis is genuinely good detective work, and the "oversized resident bitmap vs. text re-raster" framing is almost certainly correct as far as it goes. I want to build on it, but also argue that we should widen the frame before we commit to another round of fixes, because I think the solutions we've tried so far are treating symptoms of a deeper cause. Why I think we're playing whack-a-moleThe last few changes have each been locally reasonable and individually defensible, but the pattern worries me:
Every one of these was a plausible fix for one symptom on one device. The problem is we have no baseline and no cross-device measurement, so we can't actually say whether any of them helped. #789 is the clearest example: it was verified as fixing the flicker on a Pixel 8a, and it visibly regressed pan/zoom on my iPhone (side-by-side video on the PR). That's not bad luck — it's the inevitable result of optimising without a measurement that generalises across devices. The root cause is heavier than the GPU layerI measured the classical dSVG to get some hard numbers:
That geometry is the real cost, and it explains why both approaches we've tried disappoint:
In other words, #789 didn't remove the cost — it moved it from the GPU to the CPU, and the trade went the wrong way on my device. Flattening static layers (solution 3 above) helps the React/DOM side but doesn't change the fact that we're asking the browser to paint ~17k bezier commands live. The reframe: we should never be painting that geometry liveHere's the key realisation. Canvas and WebGL are not automatically faster than SVG — if you redraw 17k path commands per frame on a canvas, you've rebuilt #789's jank with a different API. The thing that actually buys performance, in SVG, Canvas, or WebGL alike, is one invariant:
The base map (landmasses, borders, province fills, names) is rendered once — never during a gesture. The overlay (units, orders, hover, selection) is tiny. So we should never be re-painting the heavy geometry during pan/zoom. Proposed architecture — three layers, and buy the viewportI'd like us to stop hand-rolling the pan/zoom/compositing machinery (we currently own a 270-line zoom wrapper, a 710-line string renderer, and a 560-line SVG-primitive builder) and instead lean on a battle-tested library that has solved interactive pan/zoom-a-big-thing on mobile a million times. My current candidate is Leaflet with The model is three clean layers:
Two things make this much lower-risk than it sounds:
Build it standalone, with diagnostics and perf as first-party concernsI feel strongly that if we're rewriting, we should not instrument the existing map — that's throwing measurement at a component we're about to delete. Instead, the new component should treat observability and performance as part of its contract from day one:
On the complex SVGs — optimise after, with one exceptionWe haven't really decided what to do about the heavy variants. My answer: defer it. The rasterise-once architecture turns the 17k-command paint from a per-frame cost into a once-per-phase cost, which makes the source SVG's visual complexity almost irrelevant to felt pan/zoom performance. Optimising the source paths first would be tuning the input to a subsystem we're about to make insensitive to it. The one piece of geometry work we need regardless is generating the decimated hit-test polygons — click/hover detection doesn't need coastline detail, and a polygon simplified at ~5px tolerance detects taps identically while being cheap to render. That's a small, testable build step that ships with v1. Cosmetic SVG optimisation (svgo, path simplification for download size / raster cost) becomes a later, independent knob we reach for only if the harness shows initial load or phase-change raster still hitches. Suggested sequence
I'd rather spend a week building the harness and a clean component than another month merging device-specific tweaks we can't measure. |
|
done. |
Uh oh!
There was an error while loading. Please reload this page.
My idea was to pick 'new experience' up as epic, but I think this is more important.
This is the root cause. We might need a different render package - or rewrite some of this. I'll investigate.
(Not making an issue because you asked me not to)
Root cause
The map is one large, monolithic SVG that react-zoom-pan-pinch scales with a CSS transform, while that SVG is force-promoted to its own GPU compositing layer (transform: translateZ(0)). The flicker is the browser repeatedly
re-rasterizing that oversized layer — it is a compositing/paint problem, not a React re-render problem.
The evidence, piece by piece:
devicePixelRatio (~2.6 on a Pixel) ≈ 15,000 px on a side — far past the GPU max-texture / tile-memory budget. When tiles can't be held resident, Chrome drops them → content blanks → flicker. Under real memory pressure it drops the
whole tile set → "all provinces flicker out at once."
expensive thing to re-shape and re-rasterize. So during each re-raster window the cheap vector below stays painted while the text-heavy above blanks — precisely the symptom you described.
entire oversized surface as one always-resident, repeatedly-re-rasterized GPU layer increases memory pressure and re-raster cost rather than reducing it.
The two symptoms, one architecture
This is also consistent with Cold War + Spice Islands being worst — they're the label/path-densest maps, so they have the most re-raster work per frame.
Recommended fixes (in order)
is the real fix but larger, since react-zoom-pan-pinch is transform-based — we'd compute viewBox from its transform state or replace the panning lib.
All reactions