NEWWorld's first AI visibility audit tool for Web3 is live.Run free audit →
PLAYBOOK Technical Last reviewed

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.

Time
30-45 minutes
Difficulty
Intermediate
Impact
High

Why this matters

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

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

Audit this fix → Free audit