Skip to content

perf(wallets): lazy-load chain providers, drop / first-load JS from 2.01 MB to 703 kB#1992

Draft
babkenmes wants to merge 2 commits into
dev-nonorepo-optimizedfrom
perf/wallet-evm-lazy-shell-split
Draft

perf(wallets): lazy-load chain providers, drop / first-load JS from 2.01 MB to 703 kB#1992
babkenmes wants to merge 2 commits into
dev-nonorepo-optimizedfrom
perf/wallet-evm-lazy-shell-split

Conversation

@babkenmes
Copy link
Copy Markdown
Collaborator

Summary

  • Each chain package now ships its connection hook + heavy SDK imports in a lazy chunk loaded via connectionRegistrar: () => import("./XConnectionRegistrar"). Replicates the EVM split across Starknet, SVM, TON, Fuel, Bitcoin, Tron, Paradex.
  • New wrapperHostsChildren: false option on defineWalletProvider renders the form as a sibling of every chain wrapper instead of nested inside the chain Suspense boundaries — so a slow chain chunk no longer hides the form.
  • Paradex's useParadexConnection no longer calls useConfig() from wagmi. EVM publishes the wagmi Config via a side store (wagmiConfigStore); Paradex reads it at call time. Lets Paradex's registrar run outside WagmiProvider, which was the last cross-chain React-context coupling.

Result

Page Before After
/ 2.01 MB 703 kB
/swap/[swapId] 1.95 MB 647 kB

grep confirms the heavy SDK chunks (WagmiProvider, StarknetProvider, TonConnectUIProvider, BigmiProvider, FuelHooksProvider) all end up in async chunks not loaded on /.

Architectural changes

Per chain, three new files alongside the slimmed index.tsx:

  • XConnectionRegistrar.tsx — lazy chunk: useXConnection, useXTransfer, plus any provider classes that statically import a chain SDK (e.g. StarknetNftProvider, SolanaAddressUtilsProvider, TonAddressUtilsProvider, BitcoinGasProvider).
  • shellInternals.tsx — the lazy chain-wrapper component + any cross-file context.
  • legacy.tsx — eager createXProvider factory + the deprecated XProvider const initializer, both moved out of the static surface so tree-shaking actually drops them when only createXShell is used.

The shell's index.tsx becomes a thin re-export surface — export { createXProvider, XProvider } from "./legacy" — and only references the heavy module via () => import("./XConnectionRegistrar").

Dropped the sync-when-cached wrapper pattern (if (Impl) return <Impl>; else return <Lazy>) — it caused subtree remounts when chunks landed because the rendered component type swapped from the lazy ref to the impl ref. React.lazy caches resolved modules itself; using the lazy ref directly keeps component identity stable.

preloadDefaultProviders() now warms both the wrapper chunk and the connection-registrar chunk for every chain, and DefaultChainShells fires it in a useEffect on mount so chunks arrive in parallel with hydration.

Pnpm overrides

Three version pins were needed for clean builds:

  • @tanstack/react-query: 5.90.11@fuels/react pulled 5.90.16 in parallel, producing a "No QueryClient set" runtime error from two distinct QueryClientContext instances.
  • rpc-websockets: 9.3.2 — newer 9.3.9 transitively requires uuid@14 (ESM-only), breaking webpack through @solana/web3.js.
  • @types/react: 19.2.3 / @types/react-dom: 19.2.3 — Radix sub-deps pulled @types/react@18.3.28, producing a ReactNode bigint mismatch in AddressWithIcon.tsx on Vercel cold builds.

Test plan

  • pnpm install — single version of @types/react, @tanstack/react-query, rpc-websockets in node_modules/.pnpm
  • pnpm -r build — every wallet package + widget compiles
  • pnpm --filter @layerswap/bridge build — bridge build green; / First Load JS reads ~700 kB
  • Bundle analyzer: chain-SDK chunks (WagmiProvider, StarknetProvider, etc.) are not in /'s chunk list
  • Cold-cache page load: form is rendered from first paint, no appear/disappear cycle
  • Click "Connect a wallet" — EVM, Starknet, SVM, TON, Fuel, Bitcoin, Tron, Paradex flows still work end-to-end
  • Paradex deposit flow (uses EVM wagmi via getEVMWagmiConfig) — verify connect + authorize still works
  • Vercel deployment succeeds (no @types/react type mismatch)

🤖 Generated with Claude Code

babkenmes and others added 2 commits May 18, 2026 14:27
Move wagmi/viem and connection/transfer/RPC-health modules into a lazy
EVMConnectionRegistrar chunk so a static import of @layerswap/wallet-evm
no longer pulls them. Legacy createEVMProvider / EVMProvider exports
move to ./legacy and retain their eager imports; shared wrapper +
WalletConnectConfig context live in shellInternals.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…de chain Suspense

Replicates EVM's lazy-registrar split for Starknet, SVM, TON, Fuel, Bitcoin,
Tron, and Paradex. Each chain now has shellInternals.tsx (lazy wrapper),
XConnectionRegistrar.tsx (heavy SDK chunk), and legacy.tsx (eager
createXProvider factory + deprecated singleton). The static index.tsx
references the registrar only via dynamic import(), so consumers of
createXShell don't pull wagmi / @starknet-react/core / @solana/wallet-
adapter-react / @tonconnect/ui-react / @fuels/react / @bigmi/react /
@tronweb3/tronwallet-adapter-react-hooks / @paradex/sdk into the
critical-path bundle.

Result: / First Load JS drops from 2.01 MB → 703 kB.

Additional changes so the form is visible from first paint:

- New wrapperHostsChildren option on defineWalletProvider. When false,
  the chain wrapper hosts only the registrar; children render as a
  sibling outside the lazy Suspense. Set false for all eight chains —
  no chain Suspense gates the form anymore.

- New wagmiConfigStore in @layerswap/wallet-evm publishes the wagmi
  Config out of the React tree. Paradex's useParadexConnection reads it
  at call time via getEVMWagmiConfig() instead of useConfig(), so the
  Paradex registrar no longer needs WagmiProvider as a React ancestor.

- Dropped the sync-when-cached wrapper pattern. The if/else swap between
  <ImplComponent> and <LazyComponent> caused subtree remounts when chunks
  landed (different component types). React.lazy caches resolved modules
  itself — using the lazy ref directly keeps component identity stable.

- preloadDefaultProviders now warms both the wrapper chunk and the
  connection-registrar chunk for each chain; DefaultChainShells fires it
  in a useEffect on mount.

Three pnpm overrides added for build-time issues uncovered along the way:
- @tanstack/react-query 5.90.11 — was duplicated via @fuels/react,
  producing "No QueryClient set" at runtime.
- rpc-websockets 9.3.2 — newer 9.3.9 transitively pulls uuid@14
  (ESM-only), breaking the Next webpack build through @solana/web3.js.
- @types/react / @types/react-dom 19.2.3 — Radix sub-deps pulled
  18.3.28 types, producing a ReactNode bigint mismatch on Vercel cold
  builds.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 18, 2026

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

Project Deployment Actions Updated (UTC)
layerswapapp Error Error May 18, 2026 5:32pm

Request Review

@babkenmes babkenmes changed the base branch from dev to dev-nonorepo-optimized May 18, 2026 17:34
@arentant arentant marked this pull request as draft May 19, 2026 14:49
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