Skip to content

perf(wallet): load the wallet modal on demand (-55% JS on /onboard) - #174

Merged
marinom2 merged 1 commit into
mainfrom
perf/lazy-wallet-modal
Jul 29, 2026
Merged

perf(wallet): load the wallet modal on demand (-55% JS on /onboard)#174
marinom2 merged 1 commit into
mainfrom
perf/lazy-wallet-modal

Conversation

@marinom2

Copy link
Copy Markdown
Owner

Why the desktop app felt heavy

Not the machine. On the reporting box hardware GL was healthy (direct rendering: Yes, OpenGL 4.6), idle CPU was 0%, and network RTT was 1.7 ms. The weight was JavaScript.

createAppKit() ran at module scope in components/providers.tsx, which app/layout.tsx renders for every route. So the landing page — and /onboard, before anyone touches a wallet — downloaded and executed the whole wallet stack up front. Measured against production: a single 1,395 KB chunk, 53% of everything /onboard shipped.

What changed

The modal is now built on the first openWallet() call (lib/appkit.ts), cached in a module-level promise so concurrent clicks share one download and createAppKit runs exactly once — calling it twice registers a second modal and account state desynchronises. Connect CTAs prefetch on hover.

Crucially, reading connection state no longer touches AppKit: ConnectButton uses wagmi's useAccount/useChainId, already in the eager bundle because WagmiProvider still wraps the tree. The button renders — and still shows a reconnected address after reload — with AppKit absent.

route before after
/onboard 708 kB 316 kB −55%
/build/bridge 752 kB 361 kB −52%
/dashboard 313 kB 314 kB unchanged

Deliberately not included

Stubbing out @coinbase/wallet-sdk. It's reachable only via wagmi's dynamic connector loading, so a resolve.alias never intercepts it — I tried it, the stub reached no chunk and total client JS didn't move (5.64 MB either way). Risk to the connect path for no gain.

Verification

tsc clean · 673 tests · eslint clean · next build succeeds.

Please click through a real wallet connect before merging. This restructures the connect path, and that's the one place a green build proves least — automated gates can't see whether the modal actually opens and signs.

`createAppKit()` ran at module scope in components/providers.tsx, and
app/layout.tsx renders that provider for every route. So anyone reading the
landing page - or an operator on /onboard who has not gone near a wallet -
downloaded, parsed and executed the entire wallet stack first: AppKit's UI,
WalletConnect, and the Coinbase SDK that `enableCoinbase: false` disables at
runtime but cannot remove from the bundle.

Measured against production, that was a single 1,395 KB chunk - 53% of
everything /onboard shipped. On a WebKitGTK desktop WebView at 3840x2160 it is
several megabytes of JavaScript to compile before the page becomes interactive,
which is what "the app feels heavy" turned out to mean. It was not the machine:
hardware GL was fine and idle CPU was 0%.

The modal now builds on the first openWallet() call (lib/appkit.ts), cached in a
module-level promise so concurrent clicks share one download and createAppKit
runs exactly once - calling it twice registers a second modal and the account
state desynchronises. Connect CTAs prefetch on hover, so the chunk is usually
warm before the click lands.

Reading connection state no longer goes through AppKit. ConnectButton uses
wagmi's useAccount/useChainId, which are already in the eager bundle because
WagmiProvider still wraps the tree - so the button renders, and still shows a
reconnected address after a reload, with AppKit absent.

  route            before    after
  /onboard          708 kB   316 kB   -55%
  /build/bridge     752 kB   361 kB   -52%
  /dashboard        313 kB   314 kB   unchanged

Verified: tsc clean, 673 tests pass, eslint clean, next build succeeds.

NOT included: stubbing out @coinbase/wallet-sdk. It is reachable only through
wagmi's dynamic connector loading, so a resolve.alias never intercepts it - the
stub reached no chunk and total client JS did not move. It would have added risk
to the connect path for no measurable gain.
@vercel

vercel Bot commented Jul 29, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
lightnode Ready Ready Preview, Comment Jul 29, 2026 8:42am

Request Review

@marinom2
marinom2 merged commit 16a2e4b into main Jul 29, 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