דמיינו שלוח ה-DeFi שלכם נטען תוך 5 שניות בגלל 20 בקשות RPC בודדות. כל בקשה ל-blockchain מוסיפה זמן אחזור. אנחנו פותרים זאת עם multicall, המאחד את כל הקריאות לקריאה אחת. התוצאה: זמן הטעינה יורד ל-0.5 שניות. זהו אתגר טיפוסי שאנו פותרים מדי יום עבור לקוחות. במשך 5 שנים ו-50+ dApps, פיתחנו גישה שמבטיחה טעינה מהירה עם לוגיקת Web3 מורכבת. מאמר זה מכסה פתרונות ספציפיים לאינטגרציה, אופטימיזציה והפרדת רכיבים.
Next.js עבור Web3 עוסק בעיקר בפתרון סתירה ספציפית: נתוני blockchain דורשים ביצוע בצד הלקוח (חיבור ארנק, חתימת עסקאות), בעוד ש-SEO וטעינה ראשונית דורשים רינדור בצד השרת. גבול שגוי בין רכיבי Server ו-Client שובר או את אינטגרציית הארנק או את הביצועים. תיעוד Next.js ממליץ להשתמש ב-Server Components עבור נתונים שאינם דורשים אינטראקטיביות.
כיצד להפריד כראוי בין רכיבי Server ו-Client ב-dApp?
שגיאות בשלב זה מובילות לחוסר התאמה בהידרציה או לקריסת האפליקציה. בואו נבחן את הפתרון.
בעיית הידרציה עם wagmi/viem
wagmi 2.x משתמש ב-localStorage ו-window.ethereum—שניהם לא זמינים בשרת. ייבוא נאיבי של useAccount ב-Server Component גורם לשגיאה. גרוע מכך—חוסר התאמה בהידרציה: השרת מרנדר "לא מחובר," הלקוח לאחר ההידרציה מציג "מחובר ל-MetaMask," ו-React מזהיר או שובר את ממשק המשתמש.
מבנה נכון:
// app/providers.tsx — CLIENT компонент, оборачивает всё приложение
'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 компонент, импортирует Providers
import { Providers } from './providers';
export default function RootLayout({ children }) {
return (
<html>
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}תוכן סטטי (navbar, footer, טקסט נחיתה) — Server Components. ארנק, יתרות, כפתורי עסקאות — Client Components עם // app/providers.tsx — CLIENT компонент, оборачивает всё приложение '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> ); } .
SSR עבור נתוני on-chain
נתוני blockchain ציבוריים (TVL של פרוטוקול, רשימת טוקנים, מחירים) ניתן לטעון בשרת. Server Components ב-Next.js (App Router) + // app/layout.tsx — SERVER компонент, импортирует Providers import { Providers } from './providers'; export default function RootLayout({ children }) { return ( <html> <body> <Providers>{children}</Providers> </body> </html> ); } עם caching:
// app/protocol/page.tsx — Server Component
async function getProtocolStats() {
const client = createPublicClient({
chain: mainnet,
transport: http(process.env.RPC_URL), // приватная переменная, не 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(); // выполняется на сервере
return <StatsDisplay stats={stats} />;
}'use client' ב-Next.js נשמר במטמון כברירת מחדל. עבור נתוני on-chain דרך viem, נדרש ניהול מפורש: fetch ב-// app/protocol/page.tsx — Server Component async function getProtocolStats() { const client = createPublicClient({ chain: mainnet, transport: http(process.env.RPC_URL), // приватная переменная, не 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(); // выполняется на сервере return <StatsDisplay stats={stats} />; } או invalidation ידני דרך Route Handlers.
מדוע multicall מאיץ טעינה פי 10?
ניהול מצב עסקאות
מחזור חיי עסקה בממשק המשתמש: fetch. כל מצב דורש משוב UI נפרד. wagmi מספק hooks לכל שלב:
function TransactionButton({ tokenId }: { tokenId: bigint }) {
const { writeContract, data: hash, isPending, error } = useWriteContract();
const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ hash });
if (isPending) return <Button disabled>Подтвердите в кошельке...</Button>;
if (isConfirming) return <Button disabled>Ожидание подтверждения ({hash?.slice(0, 8)}...)</Button>;
if (isSuccess) return <Button variant="success">Готово ✓</Button>;
return (
<Button
onClick={() =>
writeContract({
address: CONTRACT,
abi,
functionName: 'mint',
args: [tokenId],
})
}
>
Минт
</Button>
);
} עדכונים אופטימיים
עבור פעולות עם תוצאות צפויות (לייק, מעקב, toggle פשוט) — UI אופטימי דרך revalidate: 60 export const revalidate עם idle → preparing → signing → pending → confirming → success/error/function TransactionButton({ tokenId }: { tokenId: bigint }) { const { writeContract, data: hash, isPending, error } = useWriteContract(); const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ hash }); if (isPending) return <Button disabled>Подтвердите в кошельке...</Button>; if (isConfirming) return <Button disabled>Ожидание подтверждения ({hash?.slice(0, 8)}...)</Button>; if (isSuccess) return <Button variant="success">Готово ✓</Button>; return ( <Button onClick={() => writeContract({ address: CONTRACT, abi, functionName: 'mint', args: [tokenId] })} > Минт </Button> ); } /@tanstack/react-query. המשתמש רואה את השינוי מיד, והחזרה מתרחשת רק בשגיאה.
Multicall ובקשות אצווה
עבור לוחות מחוונים עם מקורות נתוני on-chain מרובים — אין לבצע קריאות RPC בודדות במקביל. Multicall3 מאגד את כל הבקשות לאחת:
const results = await publicClient.multicall({
contracts: tokenIds.map(id => ({
address: NFT_CONTRACT,
abi: erc721Abi,
functionName: 'tokenURI',
args: [id],
})),
});
viem תומך ב-useMutation באופן טבעי. עבור 100 טוקנים — בקשה אחת ל-RPC במקום 100. זה ההבדל בין 2 שניות ל-200 מילישניות לטעינת לוח המחוונים. Server Components עבור נתונים סטטיים מרנדרים פי 3 מהר יותר מ-Client Components מכיוון שהם לא כוללים את ה-JavaScript bundle להידרציה.
תהליך פיתוח dApp: מארכיטקטורה לפריסה
- ניתוח החוזה החכם ועיצוב mockups, זיהוי נקודות אינטגרציה.
- עיצוב ארכיטקטורת רכיבים: Server מול Client, layout, ניתוב.
- יישום Web3 provider (wagmi + ConnectKit/RainbowKit).
- כתיבת רכיבי שרת עבור נתוני on-chain ציבוריים עם caching.
- יישום רכיבי לקוח: חיבור ארנק, יתרות, עסקאות.
- אופטימיזציה של בקשות דרך multicall ו-React Query.
- בדיקות: יחידה (Vitest), e2e (Playwright), כיסוי מקרי קצה של עסקאות.
- פריסה ל-Vercel או Docker, הגדרת CI/CD.
פרטי סביבה חשובים
onMutate לפיתוח מקומי, onError לייצור. משתנים ללא onSettled לא נכנסים ל-client bundle — כתובות RPC עם מפתחות API צריכות להיות ללא הקידומת (בשימוש רק ב-Server Components או Route Handlers).
ספקי RPC: Alchemy, Infura, QuickNode. לייצור — ספקים מרובים עם fallback דרך const results = await publicClient.multicall({ contracts: tokenIds.map(id => ({ address: NFT_CONTRACT, abi: erc721Abi, functionName: 'tokenURI', args: [id], })), }); transport של wagmi. נקודות קצה ציבוריות של RPC (כמו eth.llamarpc.com) יש להן מגבלות קצב — אין להשתמש בייצור ללא fallback. החיסכון הממוצע בבקשות RPC משמעותי הודות ל-multicall ו-caching.
השוואה: Server Components מול Client Components עבור בלוקים טיפוסיים של dApp
| בלוק | סוג מומלץ | סיבה |
|---|---|---|
| Navbar, Footer, טקסט SEO | Server Component | ללא תלות בארנק, טעינה מהירה |
| כפתור חיבור ארנק | Client Component | דורש multicall |
| יתרת טוקן | Client Component | נתונים דינמיים, מנוי |
| TVL של פרוטוקול, רשימת טוקנים | Server Component (cached) | נתונים ציבוריים, משפר SEO |
| טופס עסקה | Client Component | אינטראקציה עם ארנק |
מה כלול בעבודה
- ארכיטקטורה: בחירת אסטרטגיית רינדור (SSR/SSG/CSR), חלוקה ל-Server/Client Components.
- אינטגרציית ארנק: MetaMask, WalletConnect, Coinbase Wallet, Ledger.
- אינטראקציה עם חוזה חכם: קריאה וכתיבה דרך viem, טיפול במחזור חיי עסקה.
- אופטימיזציה: multicall, caching, הערכת gas, fallback RPC.
- בדיקות: יחידה (Vitest), e2e (Playwright), כיסוי מקרי קצה.
- תיעוד: README, הערות קוד, דיאגרמת רכיבים.
- תמיכה: הגדרת CI/CD (Vercel/Docker), ניטור דרך Tenderly.
הערכות זמנים
| שלב | משך |
|---|---|
| Frontend בסיסי (ארנק + עסקאות) | החל משבוע |
| + SSR + אופטימיזציה + מחזור חיים מלא | החל משבועיים |
| + רכיבי UI מותאמים אישית, אנימציות | החל משלושה שבועות |
לוחות הזמנים מעודנים לאחר ניתוח החוזה החכם שלכם ו-mockups העיצוב. אנו מבטיחים שקיפות בכל שלב. צרו קשר לייעוץ—נדון בפרויקט שלכם ונציע את הפתרון האופטימלי.
טכנולוגיות
Next.js (App Router), wagmi 2.x, viem 2.x, ConnectKit או RainbowKit, @tanstack/react-query 5.x, TypeScript, Tailwind CSS. בדיקות: Vitest + Playwright ל-e2e. פריסה: Vercel או Docker.
הזמינו פיתוח עכשיו—קבלו ייעוץ לפרויקט שלכם. נעריך את ההיקף והמורכבות ונציע את הפתרון האופטימלי.







