Lazy-Load Your Wallet SDK to Fix INP and Pass Core Web Vitals
Wallet-connect SDKs are the single biggest INP killer on Web3 sites. Eager-loading them on every page costs 200-400KB of JS and 200-400ms of TBT. Here's how to lazy-load them in 30 minutes.
Why this matters
- →Wallet SDK eager-load is the most common Web3 CWV killer across 200+ TG3 audits.
- →Google's 2026 INP target is under 200ms. Eager-loaded RainbowKit alone adds 250-380ms TBT on mid-range mobile.
- →Magic Square (TG3 client) went from 380ms INP to under 200ms in 14 days using exactly this pattern.
- →1.6x organic traffic lift in 60 days from CWV improvements alone on Magic Square. The fix is 10 lines of code.
Before state (what bad looks like)
// pages/_app.tsx (Next.js example)
import { WagmiConfig, createConfig } from 'wagmi';
import { ConnectKitProvider, getDefaultConfig } from 'connectkit';
// SDK loads on every page mount, even marketing pages
const config = createConfig(getDefaultConfig({
appName: 'My dApp',
walletConnectProjectId: 'xxx',
}));
export default function App({ Component, pageProps }) {
return (
<WagmiConfig config={config}>
<ConnectKitProvider>
<Component {...pageProps} />
</ConnectKitProvider>
</WagmiConfig>
);
}
Step-by-step
Step 1: Measure your baseline INP
Open Chrome DevTools, go to Performance tab, throttle to "Slow 4G" and 4x CPU slowdown (simulates mid-range Android). Reload your homepage and capture a Performance trace. Note the Total Blocking Time (TBT) and Interaction to Next Paint (INP). Anything above 200ms INP fails CWV.
Step 2: Identify the wallet SDK in your bundle
Run a bundle analyzer. For Next.js: ANALYZE=true npm run build. For Vite: vite-bundle-visualizer. Look for @rainbow-me/rainbowkit, @web3modal/wagmi, wagmi, ethers, viem. These typically account for 200-500KB of JS on Web3 sites. That's your target for lazy-loading.
# Next.js bundle analyzer
npm i -D @next/bundle-analyzer
# Then add to next.config.js and run:
ANALYZE=true npm run build
Step 3: Mark the connect button with a data attribute
Add data-connect-wallet to every <Connect Wallet> button across your site. This is the trigger that loads the SDK. Don't use onClick handlers because we need to detect the click before the handler runs (the handler doesn't exist yet at that point).
<button data-connect-wallet className="...">Connect Wallet</button>
Step 4: Implement the dynamic import wrapper
Use Next.js's dynamic() with ssr:false to lazy-load the wallet provider. The wrapper component renders children directly until a connect button is clicked, then loads the SDK and wraps children in the actual provider. Use the "After state" code as your template.
Step 5: Move SDK initialization to the inner component
All the wagmi/RainbowKit/Web3Modal config code goes in the inner component (loaded on demand). The outer wrapper just listens for the trigger. This way, the SDK config code only runs when needed.
Step 6: Test the connect flow end-to-end
Click "Connect Wallet" on your homepage. Verify: (1) the SDK loads (Network tab shows the chunks), (2) the modal opens, (3) wallet connection completes, (4) post-connect actions work normally. The user-facing experience should be identical to before.
Step 7: Re-measure INP and confirm the improvement
Re-run the Chrome DevTools Performance trace from Step 1. INP should drop significantly (typical: 380ms → 180ms). Run PageSpeed Insights for the field-data version (CrUX data takes 28 days to update fully but the lab data updates immediately). Document the before/after for your team.
FREE WEB3 AUDIT
See where this playbook applies to your site.
Run a free Crawlux audit before you start the playbook. It tells you which fixes are most urgent.
Free first audit · No signup · 60 seconds · Full PDF report
After state (what good looks like)
// components/WalletProvider.tsx - dynamic import
import dynamic from 'next/dynamic';
import { useState } from 'react';
const WalletProviderInner = dynamic(
() => import('./WalletProviderInner'),
{ ssr: false, loading: () => null }
);
export function WalletProvider({ children }) {
const [walletNeeded, setWalletNeeded] = useState(false);
// Listen for the connect button click
if (!walletNeeded) {
return (
<div onClick={(e) => {
if (e.target.closest('[data-connect-wallet]')) {
setWalletNeeded(true);
}
}}>
{children}
</div>
);
}
return <WalletProviderInner>{children}</WalletProviderInner>;
}
// components/WalletProviderInner.tsx - actual SDK
import { WagmiConfig, createConfig } from 'wagmi';
import { ConnectKitProvider, getDefaultConfig } from 'connectkit';
const config = createConfig(getDefaultConfig({
appName: 'My dApp',
walletConnectProjectId: 'xxx',
}));
export default function WalletProviderInner({ children }) {
return (
<WagmiConfig config={config}>
<ConnectKitProvider>{children}</ConnectKitProvider>
</WagmiConfig>
);
}
How to validate the fix
- ✓PageSpeed Insights lab data: INP under 200ms.
- ✓Bundle analyzer: wallet SDK chunks not present in initial JS payload (should be in async chunks).
- ✓Network tab on first page load: zero requests to wallet SDK files.
- ✓Network tab after clicking connect: SDK chunks load, modal opens within 500ms.
- ✓After 28 days: PageSpeed Insights field data (CrUX) shows INP improvement at the 75th percentile.
Common pitfalls
Pitfall
Forgetting wagmi's context dependency
Some components downstream may use useAccount() from wagmi expecting the provider exists. Wrap those with conditional rendering or use a default value when SDK not yet loaded.
Pitfall
SSR errors from dynamic import
Always use { ssr: false } in the dynamic() options. Wallet SDKs assume browser globals (window, localStorage) and crash during SSR.
Pitfall
Auto-connect on page reload not working
If users had auto-connect enabled, they expect to be auto-connected on reload. Solution: check for the persisted connection cookie/localStorage on initial mount and trigger setWalletNeeded(true) if found. Otherwise auto-connect breaks.
Pitfall
Connect button on a different route triggering load too early
If your site has the connect button in a global header and a user lands on /blog/some-post/, the SDK loads on click but the user is already deep in content. That's actually fine; the goal is to avoid loading on initial page mount, not to never load.
Pitfall
Tree-shaking issues with wagmi providers
wagmi 2.x has been more aggressive with tree-shaking. Some imports trigger full SDK load. Use the bundle analyzer to verify your dynamic chunk doesn't accidentally include the entire SDK in the main bundle.
If something breaks: rollback
Revert WalletProvider.tsx to the eager-load version. Wallet works exactly as before within minutes. INP regresses to baseline. Consider this rollback only if the dynamic-load pattern breaks user flows; performance improvement is worth significant effort to fix forward.
Run a free Crawlux audit on this fix
Crawlux validates the schema, technical and AEO fixes from this playbook automatically. Free tier on one domain.
Run free audit →FAQ
Does this work with Vite or Vue or other frameworks?
Yes. The pattern is universal: dynamic import behind a user-action trigger. Vite has React.lazy() with Suspense. Vue has defineAsyncComponent. Same idea: only load wallet SDK on demand, not on initial mount.
What if I need wallet connection on app startup (e.g., SaaS dashboard)?
Then lazy-loading isn't the right pattern; eager-load is correct for app routes. Apply this pattern only to marketing pages and content pages where wallet connection is optional. Use route-based loading: marketing routes lazy-load, app routes eager-load.
Will this break WalletConnect's deep links?
No. WalletConnect URIs are generated when the modal opens. As long as the SDK loads when the modal needs to render, deep links work normally.
How do I handle metamask's injected provider?
MetaMask's injected window.ethereum is available without the SDK. If you want to detect MetaMask presence on initial load (without loading the full SDK), check window.ethereum?.isMetaMask. Lazy-load the SDK only when the user clicks connect.
Does this affect SEO?
Positively. Faster pages rank better. INP is a confirmed ranking signal in Google's 2026 algorithm. The SDK isn't SEO-relevant content (it's functional code), so removing it from initial load doesn't hurt anything Google indexes.
Related playbooks
Pillar guides
Audit modules
RUN YOUR FIRST AUDIT
Run the playbook against a real audit.
Get a free Crawlux audit report and use it as the baseline for the work in this playbook.
Free first audit · No signup · 60 seconds · Full PDF report
