Skip to content

perf(spa): code-split routes so D3/marked leave the initial bundle (part 1 of #101)#112

Merged
imran31415 merged 1 commit into
mainfrom
perf/spa-lazy-load
Jun 16, 2026
Merged

perf(spa): code-split routes so D3/marked leave the initial bundle (part 1 of #101)#112
imran31415 merged 1 commit into
mainfrom
perf/spa-lazy-load

Conversation

@umi-appcoder

@umi-appcoder umi-appcoder Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

What

Code-splits the SPA's top-level routes so route-only heavy dependencies leave the initial bundle — part 1 of #101 (the lazy-loading half).

src/routes/Shell.tsx: the 8 routes go from static imports to lazy() + Suspense. Each becomes its own chunk fetched on first visit, so:

  • D3 (d3-force/drag/scale/selection) — used only by Memory — moves to its own chunk.
  • marked + DOMPurify — used only by Docs — move to their own chunk.

Measured impact (yarn build)

Before After
Initial JS (entry) 274.58 kB (90.55 kB gzip), single file 73 kB entry
D3 in initial bundle separate 64 kB chunk (__data__/velocityDecay confirmed absent from entry)
marked/DOMPurify in initial bundle separate 71 kB chunk (DOMPurify confirmed absent from entry)

D3 and marked (~135 kB raw / ~47 kB gzip combined) now download only when Memory/Docs are opened.

Notes

  • A small .route-loading Suspense fallback (reduced-motion aware) covers the brief chunk fetch on route switch; local chunks resolve in a few ms.
  • app.test.tsx's route-navigation test gets a longer findBy timeout since routes now resolve through a dynamic import (it already navigates to Memory and asserts the heading renders — automated proof the lazy Memory/D3 route still mounts correctly).
  • MoreSheet stays eager (tiny, lives inside the always-mounted BottomSheet).

Verification

  • yarn tsc --noEmit clean.
  • yarn test113 passing (20 files).
  • Build chunk split verified (table above).

Scope / what's deferred

Part 2 of #101unify polling on usePoll — is intentionally not in this PR. The real target there is the module-level store pollers (tasks/memory/triggers/metrics), which should become event-driven via /api/events (#93) rather than moved to usePoll and then redone. Tracked in the issue + #93.

Addresses #101 (part 1).

🤖 Generated with Claude Code

)

Convert the 8 top-level routes in Shell.tsx from static imports to
lazy()/Suspense. Each route becomes its own chunk fetched on first
visit, so route-only heavy deps no longer ship on first paint:

  - D3 (d3-force/drag/scale/selection) — only in Memory  -> own 64 kB chunk
  - marked + DOMPurify                 — only in Docs    -> own 71 kB chunk

Entry chunk: 274.58 kB (90.55 kB gzip, single file)
          -> 73 kB entry + per-route chunks; D3/marked excluded from the
             initial download and loaded on demand.

A small Suspense fallback (.route-loading, reduced-motion aware) covers
the brief chunk fetch on route switch. app.test.tsx's route-navigation
test gets a longer findBy timeout since routes now resolve through a
dynamic import.

This is part 1 of #101 (the lazy-loading half). The polling-unification
half is intentionally deferred to land with the SSE work in #93 — the
real target there is the module-level store pollers, which should become
event-driven rather than merely moved to usePoll.

Tests: 113 passing (20 files). tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@imran31415 imran31415 merged commit b3d7556 into main Jun 16, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant