Imagine your DeFi dashboard loads in 5 seconds due to 20 individual RPC requests. Each blockchain request adds latency. We solve this with multicall, combining all calls into one. Result: load time drops to 0.5 seconds. This is a typical challenge we solve daily for clients. Over 5 years and 50+ dApps, we have developed an approach that ensures fast loading with complex Web3 logic. This article covers specific solutions for integration, optimization, and component separation.
Next.js for Web3 is primarily about resolving a specific contradiction: blockchain data requires client-side execution (wallet connection, transaction signing), while SEO and initial load require server-side rendering. An incorrect boundary between server and client components breaks either wallet integration or performance. Next.js documentation recommends using Server Components for data that does not require interactivity.
How to properly separate Server and Client components in a dApp?
Errors at this stage lead to hydration mismatches or application crashes. Let's look at the solution.
Hydration problem with wagmi/viem
wagmi 2.x uses localStorage and window.ethereum—both unavailable on the server. Naively importing useAccount in a Server Component causes an error. Even worse—hydration mismatch: the server renders "not connected," the client after hydration shows "connected to MetaMask," and React issues a warning or breaks the UI.
Correct structure:
// app/providers.tsx — CLIENT component, wraps the entire application
'use client';
import { WagmiProvider, createConfig, http } from 'wagmi';
import { mainnet, base, arbitrum } from 'wagmi/chains';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ConnectKitProvider } from 'connectkit';
const config = createConfig({
chains: [mainnet, base, arbitrum],
transports: {
[mainnet.id]: http(process.env.NEXT_PUBLIC_RPC_MAINNET),
[base.id]: http(process.env.NEXT_PUBLIC_RPC_BASE),
[arbitrum.id]: http(process.env.NEXT_PUBLIC_RPC_ARBITRUM),
},
});
const queryClient = new QueryClient();
export function Providers({ children }: { children: React.ReactNode }) {
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<ConnectKitProvider>{children}</ConnectKitProvider>
</QueryClientProvider>
</WagmiProvider>
);
}
// app/layout.tsx — SERVER component, imports Providers
import { Providers } from './providers';
export default function RootLayout({ children }) {
return (
<html>
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
Static content (navbar, footer, landing text) — Server Components. Wallet, balances, transaction buttons — Client Components with 'use client'.
SSR for on-chain data
Public blockchain data (protocol TVL, token list, prices) can be loaded on the server. Server Components in Next.js (App Router) + fetch with caching:
// app/protocol/page.tsx — Server Component
async function getProtocolStats() {
const client = createPublicClient({
chain: mainnet,
transport: http(process.env.RPC_URL), // private variable, not NEXT_PUBLIC_
});
const [tvl, totalUsers] = await Promise.all([
client.readContract({ address: PROTOCOL, abi, functionName: 'getTVL' }),
client.readContract({ address: PROTOCOL, abi, functionName: 'userCount' }),
]);
return { tvl, totalUsers };
}
export default async function ProtocolPage() {
const stats = await getProtocolStats(); // executed on server
return <StatsDisplay stats={stats} />;
}
fetch in Next.js is cached by default. For on-chain data via viem, explicit management is required: revalidate: 60 in export const revalidate or manual invalidation via Route Handlers.
Why multicall accelerates loading by 10x?
Transaction state management
Transaction lifecycle in UI: idle → preparing → signing → pending → confirming → success/error. Each state requires separate UI feedback. wagmi provides hooks for each stage:
function TransactionButton({ tokenId }: { tokenId: bigint }) {
const { writeContract, data: hash, isPending, error } = useWriteContract();
const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ hash });
if (isPending) return <Button disabled>Confirm in wallet...</Button>;
if (isConfirming) return <Button disabled>Waiting for confirmation ({hash?.slice(0, 8)}...)</Button>;
if (isSuccess) return <Button variant="success">Done ✓</Button>;
return (
<Button
onClick={() => writeContract({ address: CONTRACT, abi, functionName: 'mint', args: [tokenId] })}
>
Mint
</Button>
);
}
Optimistic updates
For operations with predictable results (like, follow, simple toggle) — optimistic UI via @tanstack/react-query useMutation with onMutate/onError/onSettled. The user sees the change immediately, rollback happens only on error.
Multicall and batch requests
For dashboards with multiple on-chain data sources — no parallel single RPC calls. Multicall3 aggregates all requests into one:
const results = await publicClient.multicall({
contracts: tokenIds.map(id => ({
address: NFT_CONTRACT,
abi: erc721Abi,
functionName: 'tokenURI',
args: [id],
})),
});
viem supports multicall natively. For 100 tokens — 1 RPC request instead of 100. That's the difference between 2 seconds and 200 milliseconds for dashboard loading. Server Components for static data render 3x faster than Client Components because they don't include the JavaScript bundle for hydration.
dApp development workflow: from architecture to deployment
- Analyze the smart contract and design mockups, identify integration points.
- Design component architecture: Server vs Client, layout, routing.
- Implement Web3 provider (wagmi + ConnectKit/RainbowKit).
- Write server components for public on-chain data with caching.
- Implement client components: wallet connection, balances, transactions.
- Optimize requests via multicall and React Query.
- Testing: unit (Vitest), e2e (Playwright), cover transaction edge cases.
- Deploy to Vercel or Docker, configure CI/CD.
Important environment details
.env.local for local development, .env.production for production. Variables without NEXT_PUBLIC_ do not end up in the client bundle — RPC URLs with API keys should be without the prefix (used only in Server Components or Route Handlers).
RPC providers: Alchemy, Infura, QuickNode. For production — multiple providers with fallback via wagmi fallback transport. Public RPC endpoints (like eth.llamarpc.com) have rate limits — do not use in production without fallback. The average savings on RPC requests are substantial thanks to multicall and caching.
Comparison: Server Components vs Client Components for typical dApp blocks
| Block | Recommended type | Reason |
|---|---|---|
| Navbar, Footer, SEO text | Server Component | No wallet dependency, fast loading |
| Wallet connect button | Client Component | Requires window.ethereum |
| Token balance | Client Component | Dynamic data, subscription |
| Protocol TVL, token list | Server Component (cached) | Public data, improves SEO |
| Transaction form | Client Component | Wallet interaction |
What is included in the work
- Architecture: choose rendering strategy (SSR/SSG/CSR), split into Server/Client Components.
- Wallet integration: MetaMask, WalletConnect, Coinbase Wallet, Ledger.
- Smart contract interaction: reading and writing via viem, handling transaction lifecycle.
- Optimization: multicall, caching, gas estimation, fallback RPC.
- Testing: unit (Vitest), e2e (Playwright), cover edge cases.
- Documentation: README, code comments, component diagram.
- Support: configure CI/CD (Vercel/Docker), monitoring via Tenderly.
Timeline estimates
| Stage | Duration |
|---|---|
| Basic frontend (wallet + transactions) | from 1 week |
| + SSR + optimization + full lifecycle | from 2 weeks |
| + Custom UI components, animations | from 3 weeks |
Timelines are refined after analyzing your smart contract and design mockups. We guarantee transparency at every stage. Contact us for a consultation—we will discuss your project and propose the optimal solution.
Tech stack
Next.js (App Router), wagmi 2.x, viem 2.x, ConnectKit or RainbowKit, @tanstack/react-query 5.x, TypeScript, Tailwind CSS. Testing: Vitest + Playwright for e2e. Deployment: Vercel or Docker.
Order development now—get a consultation on your project. We will assess the scope and complexity and propose the optimal solution.







