perf: split panel chunks by domain#4382
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummarySplits the previous ~2.54 MB eager
Confidence Score: 4/5Safe to merge with one behavioural regression to address: news feeds are over-fetched for colliding data-panel keys on non-full variants. The lazy-registration architecture, TDZ fix, and build guardrails are solid and well-tested. The condition src/config/feed-resolution.ts — the second loop in enabledNewsCategoryKeys; tests/feed-resolution.test.mts — the new lazy-path test case needs a variant covering data-panel-enabled but news-panel-disabled for colliding keys. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[App startup\nPhase 3/4] -->|await| B[panel-support chunk\nStatusPanel]
A -->|eager static import| C[main chunk\nShell + orchestration]
C -->|dynamic import on\nIntersectionObserver| D[panels-markets\nMarket/Crypto/ETF/etc.]
C -->|dynamic import on\nIntersectionObserver| E[panels-energy\nEnergy/Pipeline/Storage/etc.]
C -->|dynamic import on\nIntersectionObserver| F[panels-defense\nDefense/Hormuz/etc.]
C -->|dynamic import on\nIntersectionObserver| G[panels-news\nNews/GDELT/LiveNews/etc.]
C -->|dynamic import on\nIntersectionObserver| H[panels-economy\nEconomic/Consumer/Trade/etc.]
C -->|dynamic import on\nIntersectionObserver| I[panels-intel\nCII/Cascade/CountryDeepDive/etc.]
C -->|dynamic import on\nIntersectionObserver| J[panels-risk\nRisk/Climate/TechEvents/etc.]
D -->|replayPendingCalls| K[(enqueuePanelCall\nbuffer)]
E --> K
H --> K
I --> K
subgraph LazyPanelRegistration
L[lazyPanel registers\nload factory] --> M{Panel needed?}
M -->|Yes - deferred mount\nor eager| N[loadRegisteredPanel\ncalls load]
N --> O[Dynamic import\nchunk fetch]
O --> P[Panel instantiated\nreplayPendingCalls\nafterpanelMounted]
end
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A[App startup\nPhase 3/4] -->|await| B[panel-support chunk\nStatusPanel]
A -->|eager static import| C[main chunk\nShell + orchestration]
C -->|dynamic import on\nIntersectionObserver| D[panels-markets\nMarket/Crypto/ETF/etc.]
C -->|dynamic import on\nIntersectionObserver| E[panels-energy\nEnergy/Pipeline/Storage/etc.]
C -->|dynamic import on\nIntersectionObserver| F[panels-defense\nDefense/Hormuz/etc.]
C -->|dynamic import on\nIntersectionObserver| G[panels-news\nNews/GDELT/LiveNews/etc.]
C -->|dynamic import on\nIntersectionObserver| H[panels-economy\nEconomic/Consumer/Trade/etc.]
C -->|dynamic import on\nIntersectionObserver| I[panels-intel\nCII/Cascade/CountryDeepDive/etc.]
C -->|dynamic import on\nIntersectionObserver| J[panels-risk\nRisk/Climate/TechEvents/etc.]
D -->|replayPendingCalls| K[(enqueuePanelCall\nbuffer)]
E --> K
H --> K
I --> K
subgraph LazyPanelRegistration
L[lazyPanel registers\nload factory] --> M{Panel needed?}
M -->|Yes - deferred mount\nor eager| N[loadRegisteredPanel\ncalls load]
N --> O[Dynamic import\nchunk fetch]
O --> P[Panel instantiated\nreplayPendingCalls\nafterpanelMounted]
end
Reviews (1): Last reviewed commit: "perf: split panel chunks by domain" | Re-trigger Greptile |
…p crash (#4384) #4382 added output.onlyExplicitManualChunks (load-bearing for the panel cluster split) but its side effect pulled deck.gl's transitive deps back across the DeckGLMap boundary, forming a circular DeckGLMap -> deck-stack -> DeckGLMap chunk. At runtime that is a TDZ ("Cannot access 'X' before initialization" in deck-stack-*.js) which throws on deck.gl init, so MapContainer logs "DeckGL initialization failed, falling back to SVG map" and every user gets the old d3/SVG flat map instead of the WebGL map. The vite build already warns about the circular chunk. Fix: co-locate the DeckGLMap renderer into the deck vendor chunk so deck's deps can never split across the DeckGLMap boundary. Keeps the flag (needed to stop the panel clusters forming cycles). Verified: build has zero circular chunks (deck and panel), DeckGLMap merges into deck-stack (no separate chunk), 7 panel cluster chunks stay lazy and out of the eager modulepreload. Runtime (vite preview + Chrome): map is deckgl-mode, deck initializes, maplibre canvas renders, no deck error in console. Claude-Session: https://claude.ai/code/session_015Ae5Xavw1ZV4GMCWEsgKwe
… split Follow-up hardening from the PR #4386 review (all non-blocking; the split itself is verified correct — main.js still −108 KB, chunks lazy + excluded from the dashboard modulepreload, no circular chunks): - Rename initCheckoutOverlay -> registerCheckoutSuccessCallback: post-split it only registers the success callback (the SDK init moved to ensureCheckoutOverlayInitialized), so the old name misled callers. - De-duplicate the gear button + GEAR_SVG: extract createSettingsButton() into a tiny shared src/components/settings-button.ts used by both the eager LazyUnifiedSettings proxy and UnifiedSettings.getButton(), so the 413-char SVG and button markup/ARIA live in one place. The helper has no dependency on UnifiedSettings, so it stays in main without re-eagerizing the lazy chunk. - Unify the tab-id type: the canonical UnifiedSettingsTabId now lives in a pure components-layer leaf (src/components/settings-types.ts); app-context re-exports it and UnifiedSettings imports it, so a new tab can't drift between two declarations — and there is no components -> app backward import (passes lint:boundaries). - Suppress the spurious settings error toast when the controller is torn down mid-load (a deliberate unmount is not a user-facing failure). - Strengthen the chunk guardrail test: when a build is present, assert each secondary flow (UnifiedSettings/settings-window/checkout) actually emits its own lazy chunk — otherwise the "not preloaded" check passes vacuously if the split regresses and a module folds back into the eager entry (the #4382 onlyExplicitManualChunks failure mode). Verified: tsc clean, vite build clean with zero circular chunks, all three lazy chunks emitted and absent from dashboard.html modulepreload, lint:boundaries + lint:safe-html pass, and tests/panel-cluster-chunks.test.mjs passes 6/6 against a fresh build. Claude-Session: https://claude.ai/code/session_015Ae5Xavw1ZV4GMCWEsgKwe
* perf(startup): lazy-load settings and checkout SDK * fix(checkout): preserve active overlay init promise * refactor(checkout,settings): address review findings on the lazy-load split Follow-up hardening from the PR #4386 review (all non-blocking; the split itself is verified correct — main.js still −108 KB, chunks lazy + excluded from the dashboard modulepreload, no circular chunks): - Rename initCheckoutOverlay -> registerCheckoutSuccessCallback: post-split it only registers the success callback (the SDK init moved to ensureCheckoutOverlayInitialized), so the old name misled callers. - De-duplicate the gear button + GEAR_SVG: extract createSettingsButton() into a tiny shared src/components/settings-button.ts used by both the eager LazyUnifiedSettings proxy and UnifiedSettings.getButton(), so the 413-char SVG and button markup/ARIA live in one place. The helper has no dependency on UnifiedSettings, so it stays in main without re-eagerizing the lazy chunk. - Unify the tab-id type: the canonical UnifiedSettingsTabId now lives in a pure components-layer leaf (src/components/settings-types.ts); app-context re-exports it and UnifiedSettings imports it, so a new tab can't drift between two declarations — and there is no components -> app backward import (passes lint:boundaries). - Suppress the spurious settings error toast when the controller is torn down mid-load (a deliberate unmount is not a user-facing failure). - Strengthen the chunk guardrail test: when a build is present, assert each secondary flow (UnifiedSettings/settings-window/checkout) actually emits its own lazy chunk — otherwise the "not preloaded" check passes vacuously if the split regresses and a module folds back into the eager entry (the #4382 onlyExplicitManualChunks failure mode). Verified: tsc clean, vite build clean with zero circular chunks, all three lazy chunks emitted and absent from dashboard.html modulepreload, lint:boundaries + lint:safe-html pass, and tests/panel-cluster-chunks.test.mjs passes 6/6 against a fresh build. Claude-Session: https://claude.ai/code/session_015Ae5Xavw1ZV4GMCWEsgKwe * chore(docs): refresh generated stats * docs: sync component count references
…S loop (#4391) #4382 flipped the `live-news` video panel from an eager assignment set BEFORE the CANONICAL_FEEDS news-panel loop to a lazy registration AFTER it, and swapped the collision guard from the dynamic `this.ctx.panels[key]` to a static `COLLIDING_NEWS_PANEL_KEYS` set that omits `live-news`. Net effect: the loop now creates a generic NewsPanel for the `live-news` key (fed by `CANONICAL_FEEDS['live-news']`, the energy-headlines RSS block). It registers first, and `lazyPanel()`'s dedup guard then blocks the real `LiveNewsPanel` (24/7 video). On the live dashboard the flagship "LIVE NEWS" panel rendered the empty news state — "No items in the last 7 days" — instead of the video streams. Fix: skip `live-news` in the CANONICAL_FEEDS loop so the dedicated video panel owns the key on every variant. `happy` has no `live-news` panel at all, and every other variant has non-empty default channels, so no variant legitimately wants a generic RSS panel here — this restores pre-#4382 behavior exactly. Adds a source-level regression guard in live-news-panel-guard.test.mts. Claude-Session: https://claude.ai/code/session_01EdPTDeKmCTLD2mqWrxB8kc
…S loop #4382 flipped the `live-news` video panel from an eager assignment set BEFORE the CANONICAL_FEEDS news-panel loop to a lazy registration AFTER it, and swapped the collision guard from the dynamic `this.ctx.panels[key]` to a static `COLLIDING_NEWS_PANEL_KEYS` set that omits `live-news`. Net effect: the loop now creates a generic NewsPanel for the `live-news` key (fed by `CANONICAL_FEEDS['live-news']`, the energy-headlines RSS block). It registers first, and `lazyPanel()`'s dedup guard then blocks the real `LiveNewsPanel` (24/7 video). On the live dashboard the flagship "LIVE NEWS" panel rendered the empty news state — "No items in the last 7 days" — instead of the video streams. Fix: skip `live-news` in the CANONICAL_FEEDS loop so the dedicated video panel owns the key on every variant. `happy` has no `live-news` panel at all, and every other variant has non-empty default channels, so no variant legitimately wants a generic RSS panel here — this restores pre-#4382 behavior exactly. Adds a source-level regression guard in live-news-panel-guard.test.mts. Claude-Session: https://claude.ai/code/session_01EdPTDeKmCTLD2mqWrxB8kc
…(U2, #4478) (#4483) * perf(main.js): defer bases-expanded (~48KB) off the eager boot graph MILITARY_BASES (merged from the ~48KB bases-expanded table) was held on the eager boot graph by FOUR static import edges: geo.ts and military-surge (both eager via data-loader), plus the eager country-intel and related-assets consumers. A manualChunks rule alone can't defer it — a static import evaluates the module regardless of chunk placement (cf. #4382). - Relocate MILITARY_BASES + mergeAndDeduplicateBases + US_DOMESTIC_BASES out of the eager geo.ts into a dedicated src/config/military-bases.ts (sync export). - Break the eager edges: data-loader dynamic-imports military-surge at its two surge call sites; country-intel and related-assets lazy-cache the bases via dynamic import (mirroring the existing UNDERSEA_CABLES/datacenter pattern). - Repoint lazy consumers (Map, DeckGLMap, GlobeMap, search-manager, e2e harness) to @/config/military-bases; drop MILITARY_BASES from the @/config barrel. - manualChunks isolates military-bases-data (43.5KB / 9.3KB gzip); the dashboard-eager-chunks guard pins it off the entry chunk + modulepreload. Verified: tsc clean; build emits no circular-chunk warning; eager-chunk guard 30/30 (chunk only dynamically referenced); 54 behavior tests pass; runtime boot smoke renders the dashboard + map with no TDZ/chunk-load error. U2 of the main.js eager diet (#4476). Closes #4478. Claude-Session: https://claude.ai/code/session_017L8nACBdvQ5Sj7mTBfEBhy * refactor(country-intel): dedup in-flight military-bases preload promise Match the established lazy-loader pattern (related-assets preloadBaseIndex / preloadCableIndex): share a single in-flight import() promise and reset it on error so concurrent preloads don't each fire an import and a failed load can retry. Harmless at runtime (dynamic import is idempotent) but aligns with the peer loaders — addresses the Greptile review note on #4483. Claude-Session: https://claude.ai/code/session_017L8nACBdvQ5Sj7mTBfEBhy * fix(main): harden lazy military base loading * chore(docs): refresh generated stats * test(main): allow combined country brief lazy preload
Summary
Fixes #4371.
PANEL_CLUSTERsplit so panel modules build into per-domain chunks instead of one eagerpanels-*bundle.Intent
#4381 made the shell contentful before hydration, but left the first-wave panel payload intact. This PR focuses on the next bottleneck: splitting the previous ~2.54 MB
panels-*eager chunk into domain chunks without reintroducing the TDZ crashes that blocked the earlier split.I reviewed draft PR #3946 as a required decision point. I did not reuse it because it was an older draft for #3944 with conflicts/failing checks and stale assumptions against current
origin/mainafter #4381. This branch starts fresh from currentmainwhile adapting the durable approach: lazy service clients plus documented panel domain chunks.Validation Matrix
npm run worktree:bootstrapnpm run build:fullpanels-DXgUQe1q.jswas 2,541,859 decoded / 671,155 gzip and modulepreloaded bydashboard.htmlnode --test tests/panel-cluster-chunks.test.mjs tests/feed-resolution.test.mtsnpx tsx --test --test-concurrency=16 tests/mission-presets.test.mts tests/product-catalog-freshness.test.mjs tests/panel-config-guardrails.test.mjs tests/panel-layout-dynamic-import-guard.test.mts tests/threat-timeline-panel.test.mtsnpx tsx --test --test-concurrency=16 tests/frontend-cii-source-of-truth.test.mts tests/live-news-panel-guard.test.mts tests/load-all-data-scheduling.test.mtsnpm run typechecknpm run build:fullnpm run test:datagit diff --checknpm run preview -- --host 127.0.0.1 --port 4172Final built chunks:
main-Bhhdo0cL.jsPanel-4kdyZ6ZX.jspanel-support-BwGUOgTA.jspanels-defense-DTj2y-BO.jspanels-economy-DL53tGCX.jspanels-energy-B4mBai0f.jspanels-intel-DzJbWByQ.jspanels-markets-DsIk7GaP.jspanels-news-qEVHNlJe.jspanels-risk-DbmiqzOf.jsEntry HTML checks:
dashboard.htmlpanel/support modulepreloads:0main-Bhhdo0cL.jsstatic panel/support imports:0Review Gates
mainhas no static panel/support imports.dist/exists.Documentation
No user-facing docs changed. The durable contract is encoded in
vite.config.tscomments andtests/panel-cluster-chunks.test.mjs.Screenshots / UI Evidence
No intentional UI change. I captured a local browser smoke screenshot while verifying startup, but the evidence for this PR is bundle/runtime: all panel domain chunks dynamically import successfully, no TDZ/ReferenceError/page errors, and the entry HTML no longer modulepreloads panel/support chunks.
Residual Findings
mainis larger after making manual chunks explicit. That keeps app-shared dependencies from dragging panel chunks back into the entry graph, but the remainingmaindiet belongs to follow-up perf: main.js entry is 744 KB with 442 KB (59%) unused — code-split/tree-shake the eager chunk #4379.