Skip to content

perf: set initial-route and scanner-worker bundle budgets - #229

Merged
truthixify merged 3 commits into
wraith-protocol:developfrom
JafetCHVDev:fix/issue-219-bundle-budgets-lazy-load
Oct 18, 2026
Merged

truthixify merged 3 commits into
wraith-protocol:developfrom
JafetCHVDev:fix/issue-219-bundle-budgets-lazy-load

Conversation

@JafetCHVDev

@JafetCHVDev JafetCHVDev commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Closes #219

The production build had no route or worker size budgets: the entry chunk was 3,701.60 kB and the Stellar scanner worker was 982 kB, both unbounded and eagerly shipped regardless of which chain or page the user actually needed.

  • Lazy-loaded every route (App.tsx): all 13 page components now load via React.lazy() behind a Suspense boundary instead of being statically imported into the entry.
  • Lazy-loaded the per-chain send/receive components (pages/Send.tsx, pages/Receive.tsx): each chain's wallet SDK (wagmi/RainbowKit for Horizen, Freighter + @stellar/stellar-sdk for Stellar, @solana/wallet-adapter-* for Solana, @ckb-ccc/* for CKB) now loads only when that chain is actually selected, instead of all four loading up front.
  • Added RouteErrorBoundary + RouteFallback: a lazy chunk's import() can reject (stale deploy, flaky network); the boundary catches that and offers Retry (re-attempt the same import) or Reload (pick up the current deploy's asset manifest), with a loading fallback for the normal case.
  • Defined bundle budgets in CI (scripts/bundle-budget-gate.mjs, wired into .github/workflows/ci.yml via pnpm build:budget-check): checks the true initial-load set (parsed from dist/index.html's <script type="module">/<link rel="modulepreload"> tags, so anything only reachable via a dynamic import() is correctly excluded), the scanner worker chunk, and every other (lazy route / lazy chain component / third-party vendor) chunk against three budgets. Has its own unit tests (scripts/bundle-budget-gate.test.mjs).

Result: the initial entry chunk went from 3,743.93 kB to 3,015.65 kB (~19% smaller) on this machine's build. StellarSend (384 kB), StellarReceive (107 kB), HorizenReceive (24 kB), Vault (36 kB), and StellarSplit (32 kB) are now separate, on-demand chunks instead of being baked into the entry.

What I tried for the scanner worker that did not help (documented so it isn't retried)

I expected @stellar/stellar-sdk/minimal (or importing @stellar/stellar-base directly, bypassing the SDK wrapper) to shrink the worker, since the worker only parses already-fetched XDR and never touches Horizon/RPC/axios. Empirically, with a fully cleared build cache, both made the worker larger (1.77 MB and 1.42 MB respectively, up from 982 kB), not smaller — @stellar/stellar-sdk's root and /minimal entry files are byte-identical in this SDK version, and switching the import specifier only cost tree-shaking benefit without removing any real code. I reverted both attempts and left the worker's import as-is; its budget here (1.1 MB) reflects that measured floor rather than a further reduction.

Not done in this PR (flagged as a clear next step, not silently dropped)

The remaining ~3 MB in the entry is dominated by main.tsx unconditionally mounting all four chains' wallet provider trees (WagmiProvider/RainbowKitProvider, Solana's ConnectionProvider/WalletProvider, ccc.Provider) regardless of which chain is active. Deferring provider mounting until a chain is selected would be the next lever here, but it's a correctness-sensitive change to the app's root provider tree (several components read wallet hooks that assume their provider is always mounted) that deserves its own PR and review rather than being bundled into this one.

Test plan

  • pnpm build (clean, with node_modules/.vite and dist/ cleared first)
  • pnpm build:budget-check passes against the resulting dist/
  • pnpm format:check passes repo-wide
  • pnpm test:unit — same 4 pre-existing failures as on develop (unrelated: a date-bucketing test in portfolio.test.ts and a jsdom env issue in nameWatchlistStore.test.ts, neither file touched here), all other 339 tests pass including the 11 new tests for the budget gate
  • Ran pnpm dev and drove it in a real browser: loaded /send and /receive, cycled through all four chains (Horizen, Stellar, Solana, CKB) on each via the chain switcher, confirmed the correct wallet-connect control renders for each (Freighter for Stellar, Phantom's "Select Wallet" for Solana, etc.) and that no console errors or failed chunk loads appeared at any point

@vercel

vercel Bot commented Oct 16, 2026

Copy link
Copy Markdown

@JafetCHVDev is attempting to deploy a commit to the truthixify's projects Team on Vercel.

A member of the Team first needs to authorize it.

@drips-wave

drips-wave Bot commented Oct 16, 2026

Copy link
Copy Markdown

@JafetCHVDev Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@truthixify truthixify left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Retry does not retry a failed route import. React.lazy caches the rejected import promise; clearing the error boundary state simply renders the same lazy component and throws the same cached rejection again. This is especially visible for the transient-network case the UI says it handles. Please recreate or remount a fresh lazy loader and import attempt on retry, and add a test covering rejected-first then successful-second chunk loading. Using reload as the only recovery is also coherent if that is the intended behavior.

@JafetCHVDev

Copy link
Copy Markdown
Contributor Author

Thanks for catching that. You were right: Send/Receive/etc. were lazy() singletons at module scope in App.tsx, so React.lazy's rejected-promise cache on that one object persisted for the tab's whole lifetime. Retry's setState({error: null}) just re-rendered the same cached-rejected lazy component, so it re-threw immediately.

Fixed in src/components/RouteErrorBoundary.tsx: added a LazyPage component that builds the lazy() inside useMemo(() => lazy(loader), [loader]), tied to that component instance rather than module scope. RouteErrorBoundary's retry already swaps its rendered output between the fallback and children (a structural change at that position), which remounts the children subtree from scratch, so LazyPage's useMemo cache is naturally discarded and a fresh lazy component (and fresh import()) is built. App.tsx's 13 lazy() consts became plain loader functions, each route now renders .

Added RouteErrorBoundary.test.tsx: a loader that rejects until the test flips a flag and clicks Retry, then resolves; asserts the boundary renders the loaded content rather than getting stuck replaying the rejection (verified this fails against both the original code and a bare lazy() with no LazyPage).

Also merged upstream/develop to pick up #230 and #227 (resolved a real conflict in App.tsx, both touching the effects block; auto-merged cleanly) and reran the full verification: pnpm format:check, pnpm test:unit (348 passing, 2 pre-existing unrelated portfolio.test.ts date-bucketing failures, same as on develop), clean pnpm build, and pnpm build:budget-check, all green.

@truthixify truthixify left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Retry now remounts LazyPage and creates a fresh React.lazy instance, so a rejected dynamic import is not replayed from React\047s cached promise. The route and bundle-budget suites (12 tests) and production build pass locally.

@truthixify
truthixify merged commit d2ba3aa into wraith-protocol:develop Oct 18, 2026
1 of 2 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.

[Wave 10] Set initial-route and scanner-worker bundle budgets

2 participants