perf(globe): lazy-load GlobeMap — remove three.js/globe.gl from initial bundle#3244
perf(globe): lazy-load GlobeMap — remove three.js/globe.gl from initial bundle#3244shivam8764 wants to merge 1 commit intokoala73:mainfrom
Conversation
…ial bundle (koala73#3111) Static import of GlobeMap pulled globe.gl + three.js (~1,781 kB uncompressed) into main.js for all users regardless of whether they ever activate globe mode (the vast majority). Converted both instantiation sites (init() and switchToGlobe()) to dynamic import('./GlobeMap'), deferring the globe-stack and three chunks until the user opts in. Also splits globe.gl and three.js into named manualChunks for independent caching, adds rollup-plugin-visualizer behind ANALYZE=1 (npm run build:analyze), and includes Phase 24 Lighthouse audit reports and postmortem. Before: main.js 2,836 kB → After: main.js 1,055 kB (−63%) Desktop Lighthouse Performance: 54 → 79
|
@shivam8764 is attempting to deploy a commit to the World Monitor Team on Vercel. A member of the Team first needs to authorize it. |
koala73
left a comment
There was a problem hiding this comment.
Review
Why this PR: Real win. Lazy-loading globe.gl+three.js drops main.js 2.8 MB → ~1 MB and desktop Lighthouse 54 → 79. The shape is right — type-only import at the top, dynamic import('./GlobeMap') at the two construction sites, manualChunks rule splitting three and globe-stack, plus rollup-plugin-visualizer behind ANALYZE=1.
That said, making GlobeMap async introduced three real correctness regressions versus the synchronous path it replaced. All three need fixing before merge.
Blocking
1. init() never rehydrates after the chunk resolves — cold-start users lose data/layers/handlers
src/components/MapContainer.ts:206
import('./GlobeMap').then(({ GlobeMap }) => {
this.globeMap = new GlobeMap(this.container, this.initialState);
// ❌ no restoreViewport(), no rehydrateActiveMap()
});Compare with switchToGlobe at line 246 which does call both. Users who already have globe mode saved in storage (src/app/panel-layout.ts:793) hit the init() path: the constructor returns synchronously with useGlobe=true but globeMap=null, so every downstream bootstrap call — initEscalationGetters() one line after construction, the first flight of setEarthquakes/setWeatherAlerts/…, callback wiring via onStateChanged/setOnLayerChange/… — caches values against a null globe and ?. short-circuits the forward call. Nothing replays the cache once new GlobeMap(...) finally runs. Result: cold-start in globe mode can come up with missing markers, handlers, and viewport state until a manual refresh or mode toggle.
Fix: add this.restoreViewport(this.initialState, null) (or equivalent) + this.rehydrateActiveMap() inside the .then, symmetric with switchToGlobe.
2. No cancellation / version guard on the in-flight import
src/components/MapContainer.ts:206 and :246
Both dynamic imports fire their .then unconditionally. If the user flips to globe and back to flat before the chunk resolves, the stale promise still runs new GlobeMap(this.container, …) on top of the now-active flat map — recreating a globe in the container, leaking listeners/renderers, and leaving useGlobe/globeMap out of sync with what's actually visible. The same applies if the container/MapContainer is destroyed during the wait. The old synchronous path couldn't race; this one can.
Fix: gate each .then with a token check, e.g. bump this.initGen++ at the start of init/switchToGlobe/switchToFlat/destroy, capture the value, and if (gen !== this.initGen) return; inside the callback. Or abort via a cancellation flag tracked on this.
3. No failure path on the dynamic import
src/components/MapContainer.ts:206-208 and :246-250
Neither call has a .catch. A chunk-load failure (transient CDN blip, stale HTML pointing at a chunk hash removed by a later deploy — both routine in production) now leaves the map in a dead state: useGlobe=true, globeMap=null, no Sentry event, no fallback to flat, no retry. Previously an eager import would have failed at build time — this is a new runtime failure mode that needs an explicit rollback.
Fix: .catch(err => { Sentry.captureException(err); this.useGlobe = false; this.useDeckGL = this.shouldUseDeckGL(); this.init(); }) (combined with the cancellation guard from #2).
Should fix
4. ~3.3 MB of Lighthouse reports committed to the repo. docs/lighthouse/desktop-after.report.html (1.1 MB), desktop-after.report.json (1.3 MB), mobile-after (960 kB) — the raw reports account for ~45k of the 49k-line diff. They'll churn and bloat permanently on every re-audit. Keep the PHASE-24-POSTMORTEM.md; move the reports to a GH Actions artifact or cloud link and add docs/lighthouse/*.report.* + docs/lighthouse/mobile-after* to .gitignore.
5. docs/lighthouse/mobile-after has no file extension — looks like a truncated CLI output filename. Rename to mobile-after.report.{html,json} or drop.
Nits
vite.config.ts:821—id.includes('/globe-kapsule/')doesn't match anything in the globe.gl dep tree;kapsuleis a separate npm package and is already covered by the/kapsule/branch on the same line. Drop.switchToGlobesynchronously destroys the flat map and then waits on a ~1.7 MB chunk, so the user sees an empty container for potentially hundreds of ms on mobile. A lightweight skeleton during the import would help; follow-up scope is fine.build:analyze+ visualizer is a nice permanent addition.dist/bundle-analysis.htmlis already under the gitignoreddist/, good.
globe.gl + three.js were getting pulled into main.js via a static import in MapContainer, even for users who never touch globe mode. switched it to a dynamic import so those chunks only load when someone actually enables the globe.
main.js went from 2.8MB down to ~1MB. desktop lighthouse perf went 54 -> 79.
also split three and globe-stack into their own named chunks (better caching), and added rollup-plugin-visualizer so you can run
ANALYZE=1 npm run buildto get a treemap.threw in the lighthouse reports + a postmortem under docs/.
still a ways off from the 100 target — panels.js is the next big one (2.2MB, loads all ~50 panels regardless of what's visible). noted in the postmortem.
Fixes #3111