Skip to content

perf(globe): lazy-load GlobeMap — remove three.js/globe.gl from initial bundle#3244

Draft
shivam8764 wants to merge 1 commit intokoala73:mainfrom
shivam8764:fix/issue-3111
Draft

perf(globe): lazy-load GlobeMap — remove three.js/globe.gl from initial bundle#3244
shivam8764 wants to merge 1 commit intokoala73:mainfrom
shivam8764:fix/issue-3111

Conversation

@shivam8764
Copy link
Copy Markdown

@shivam8764 shivam8764 commented Apr 20, 2026

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 build to 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

…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
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 20, 2026

@shivam8764 is attempting to deploy a commit to the World Monitor Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions github-actions Bot added the trust:caution Brin: contributor trust score caution label Apr 20, 2026
Copy link
Copy Markdown
Owner

@koala73 koala73 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:821id.includes('/globe-kapsule/') doesn't match anything in the globe.gl dep tree; kapsule is a separate npm package and is already covered by the /kapsule/ branch on the same line. Drop.
  • switchToGlobe synchronously 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.html is already under the gitignored dist/, good.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

trust:caution Brin: contributor trust score caution

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Phase 24: Validation & Before/After Postmortem

2 participants