At any given moment there are several thousand aircraft transmitting over the contiguous US, and each one is moving, turning, climbing, and reporting a new position roughly once a second. Drawing that on a map that stays smooth under your finger is harder than it sounds. Here's how we got from “the tab fan spins up” to a map that holds 60fps with twelve thousand aircraft on screen.
The naive version, and why it dies
The obvious first approach is one DOM element per aircraft — an absolutely-positioned marker you move around with CSS transforms. It's easy to reason about and it works beautifully… up to a few hundred aircraft. Then it falls off a cliff.
The problem is that every position update touches the DOM, and the browser has to recompute layout and repaint. With a few hundred markers updating once a second, that's survivable. With several thousand, the main thread spends its entire frame budget on layout thrash and garbage collection from the churn of style recalculations. You blow past 16.6 milliseconds — the budget for a 60fps frame — and the map starts to stutter. Pan the map while that's happening and it feels like dragging through mud.
Step one: move rendering to the GPU
The fix is to stop treating aircraft as DOM elements and start treating them as data the GPU draws. We render the fleet as a GL layer on top of the map — thousands of sprites in a handful of draw calls instead of thousands of individual DOM nodes. The GPU was built for exactly this: throwing many small textured quads at the screen is what it does in its sleep.
This is the single biggest lever. Once aircraft are GPU-rendered, the cost of drawing 12,000 of them is closer to the cost of drawing 1,000 of them than the DOM approach ever allowed. Layout and repaint stop being the bottleneck because there's nothing to lay out — it's a texture, not a tree of elements.
Step two: don't resend the world
Rendering is only half the battle. The other half is getting fresh positions to the browser without drowning it. The naive version pushes the full state of every aircraft every second. That's a lot of JSON to parse, and most of it is unchanged — a plane at cruise reports a position barely different from a second ago.
Instead we send deltas over a WebSocket: only what changed since the last tick. A new aircraft, an updated position, an aircraft that dropped off. The browser applies the diff to the state it already has rather than rebuilding the world from scratch. That cuts both the bytes on the wire and the parsing cost on the main thread.
Step three: batch updates to the frame clock
Even with deltas, updates arrive in bursts — a clump of messages, then quiet, then another clump. If you apply each message the instant it lands, you trigger a re-render per message and fragment your frame budget into useless slivers.
So we coalesce. Incoming updates accumulate in a buffer and get flushed once per animation frame, aligned to the browser's paint clock viarequestAnimationFrame. Twenty messages that arrive within a 16ms window become a single state update and a single render. The map sees one coherent batch per frame instead of twenty interruptions.
The price of pretty animation
Aircraft positions arrive about once a second, but a 60fps map paints sixty times a second. If you only move a plane when a new position arrives, it jumps once a second — technically correct, visually ugly. To make it glide, you interpolate: between real fixes, you advance each aircraft along its last known heading and speed so the motion looks continuous.
That smoothness has a cost. Interpolating every aircraft every frame is CPU work that scales with fleet size, and it's the kind of work that quietly cooks a phone. So it's a budget decision: how much visual polish is worth how much battery and thermal headroom on a mid tier mobile device? We tune the interpolation to look smooth on the hardware most people actually carry, not the workstation it was built on.
Step four: keep the heavy chunk off the critical path
The GL map engine is big — well over a megabyte of JavaScript before you've drawn a single aircraft. Shipping that in the initial bundle means every visitor pays for it before the page is interactive, including the ones who came to read a pricing page and never touch the map.
So the entire map subtree is code-split behind a lazy boundary. The shell, the navigation, and the text render immediately; the map engine streams in afterward and mounts when it's ready. First paint doesn't wait on the part of the app that needs a GPU.
The honest tradeoffs
None of this is free, and we made deliberate choices:
- Memory. Holding twelve thousand aircraft plus their recent trails in memory costs RAM. We cap trail history and prune aircraft that have gone stale rather than keeping everything forever.
- Mobile thermals. Continuous GL rendering plus interpolation is the kind of sustained load that warms a phone. We throttle work when the map isn't the focus and dial back animation on weaker devices.
- Complexity. Delta protocols, batching buffers, and interpolation state are all more moving parts than “render the markers.” The payoff — a map that stays smooth at scale — is worth the extra surface, but it's real surface to maintain.
The throughline: a real-time map lives or dies on whether it feels alive. Stutter reads as broken even when the data is perfect. Getting to a smooth 60fps with the whole sky on screen is less about one clever trick and more about refusing to waste the frame budget at every layer — the GPU, the wire, the parse, the paint clock, and the bundle.