perf(frontend): cache static deck.gl layers, code-split heavy views, vendor chunking#324
Merged
Merged
Conversation
…vendor chunking Rendering: - Add LayerCache: static layer groups (infra, airspace, weather, GDELT, clusters, ...) are memoized across rAF ticks and return identical Layer instances while inputs are unchanged, so deck.gl skips diffing and GPU attribute regeneration for them; only entity/satellite/trail layers rebuild per frame - Quantize pulse animations (aurora, jamming, FIRMS, dark vessels, holding patterns, kiwi node) to 10 Hz ticks instead of per-frame updateTriggers - Memoize terminator polygon per minute (was recomputed + reallocated 60x/s) - StarField: globalAlpha instead of per-star rgba string formatting - interpolation: hoist spherical-math constants out of the per-entity path Loading: - Lazy-load TacticalMap, OrbitalMap, IntelGlobe, DashboardView and RadioTerminal behind Suspense boundaries - vite manualChunks: deck.gl, maplibre, mapbox, echarts and react split into cacheable on-demand vendor chunks; isolate Vite's preload helper so the entry no longer transitively preloads vendors - Map library CSS moved into the adapters so only the selected engine's stylesheet is downloaded - Consolidate Google Fonts into index.html; drop render-blocking CSS @import Verified: lint, typecheck, 272/272 unit tests, production build chunk graph (entry preloads only preload-helper + react-vendor). https://claude.ai/code/session_01BksbmT1NKcWRSiPA9FUh6B
…ch-up surge Satellites loaded, drifted slowly, then periodically surged across the globe in unison. Three root causes, three fixes: - Anchor dead reckoning to the position's source epoch (entity.time) instead of receive time. Sweep chunking + Kafka + WS delivery make positions 1-10s old on arrival; receive-time anchors rendered every satellite persistently behind reality until the next sweep corrected it. DRState now carries serverTime (position epoch, projection anchor) and blendTime (receive time, blend-progress anchor). New entities seed blendTime = serverTime so they appear at their true current position immediately. Guarded against clock skew / wrong units via drAnchorTime(). - Teleport guard in interpolatePVB: gaps beyond max(2 deg, speed x interval x 3) snap in one frame instead of racing the visual across the map; raw (unwrapped) lon delta so antimeridian crossings snap rather than smoothing the long way around. - Frame-stall reset in useAnimationLoop: after a >1s frame gap (hidden tab, GC, shader compilation) clear visualState so visuals re-seed at their targets in one clean snap instead of a global catch-up surge. Also: satellites now reject out-of-order/duplicate sweeps (same lastSourceTime guard the aircraft/ship path had) and seed expectedInterval at the real 15s sweep cadence. Verified: lint, typecheck, 278/278 tests (6 new covering epoch anchoring and the teleport guard), production build. https://claude.ai/code/session_01BksbmT1NKcWRSiPA9FUh6B
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two-front performance pass on the frontend: stop the 3D globe's render loop from rebuilding the world every frame, and stop the initial page load from shipping every map/chart engine up front. A third commit fixes the satellite "catch-up surge" on globe load.
Rendering (globe frame time)
The
useAnimationLooprAF loop was callingcomposeAllLayers()at 60 fps, constructing ~40–50 new deck.gl Layer instances per frame — including static groups (cables, towers, airspace, NWS, GDELT, clusters, …) whose data changes every 30 s–15 min. Several builders also allocated newdataarrays per frame, forcing deck.gl to regenerate and re-upload GPU attribute buffers continuously.LayerCache(src/layers/layerCache.ts): every static layer group incomposeAllLayers()is memoized on a deps array (React-hooks-styleObject.iscontract). Unchanged groups return the identical Layer instances, so deck.gl skips diffing and attribute work for them outright. Only entity/satellite/trail layers — whose positions genuinely change each frame — still rebuild per frame. Each overlay owns its own cache (Layer objects hold per-deck state).globalAlphainstead of formatting ~320rgba()strings per frame; interpolation hot path no longer recomputes trig constants per entity.Loading (initial latency)
lazyView()helper with built-in Suspense — the view-switch JSX is untouched.vite.config.ts(function-formmanualChunks):deck-gl(1.03 MB),maplibre(1.07 MB),mapbox(1.72 MB),echarts(1.14 MB),react-vendor. Vite's virtual preload helper is isolated into its own chunk — otherwise Rollup parks it inside a vendor chunk and the entry transitively preloads that vendor (the object form ofmanualChunkshas the same failure mode, which is why the function form is used).index.htmllink resolving in parallel via preconnect; removed the render-blocking@importfrom the bundled CSS.Result: the entry now modulepreloads only
preload-helper(~1 kB) +react-vendor(60 kB gzip) — down from an initial graph that eagerly included all of deck.gl/maplibre/mapbox (~5 MB+). Vendor chunks are byte-stable across releases, so returning clients hit the HTTP cache.Satellite catch-up surge fix
Satellites loaded, drifted slowly along their orbits, then periodically all surged across the globe at once. Root causes and fixes:
time(the SGP4 propagation epoch), but the client anchored DR to receive time — sweep chunking + Kafka + WS delivery make positions 1–10 s old on arrival, so satellites rendered persistently behind reality until the next 15 s sweep "corrected" them.DRStatenow splitsserverTime(position epoch — projection anchor) fromblendTime(receive time — blend-progress anchor); new satellites appear at their true, epoch-projected current position immediately. Guarded against clock skew / wrong units (drAnchorTime()).interpolatePVB: visual→target gaps beyondmax(2°, speed × interval × 3)snap in one frame instead of racing across the map; the longitude delta is deliberately unwrapped so antimeridian crossings snap to the far side rather than smoothing the long way around the globe.expectedIntervalseed.Verification
pnpm run lint— clean (0 warnings)pnpm run typecheck— cleanpnpm run test— 278/278 passed (6 new tests covering epoch anchoring and the teleport guard)pnpm run build— chunk graph inspected indist/: vendors are on-demand, entry preloads nothing heavy, mapbox vs maplibre load exclusivelyFollow-ups (out of scope)
public/world-countries.jsonis 14 MB of raw GeoJSON fetched at runtime — simplifying geometry (mapshaper/TopoJSON) would cut multi-second map start on slow links.Task logs:
agent_docs/tasks/2026-06-12-frontend-performance-optimization.md,agent_docs/tasks/2026-06-12-satellite-position-jump-fix.mdhttps://claude.ai/code/session_01BksbmT1NKcWRSiPA9FUh6B