Оптимизация производительности dApp
Большинство dApp медленные по одной причине: каждый компонент делает свой RPC вызов, и они не координируются. Страница с 10 компонентами — 10 параллельных запросов к Infura, каждый с latency 100–300ms. Пользователь видит постепенно появляющийся UI с loading state везде. Исправление этого — главная задача оптимизации.
RPC оптимизация
Multicall и батчинг
multicall3 — контракт, задеплоенный на большинстве EVM сетей по адресу 0xcA11bde05977b3631167028862bE2a173976CA11. Позволяет выполнить N view вызовов в одном RPC запросе:
import { useReadContracts } from 'wagmi'
// Вместо 3 отдельных useReadContract:
const { data } = useReadContracts({
contracts: [
{ address: tokenA, abi: erc20Abi, functionName: 'balanceOf', args: [userAddress] },
{ address: tokenB, abi: erc20Abi, functionName: 'balanceOf', args: [userAddress] },
{ address: tokenC, abi: erc20Abi, functionName: 'balanceOf', args: [userAddress] },
],
})
wagmi v2 автоматически батчит вызовы через multicall3 если включена опция batch: { multicall: true } в transport конфиге. Проверьте что она включена — по умолчанию да, но иногда отключают при дебаггинге и забывают вернуть.
Кэширование с TanStack Query
wagmi построен поверх TanStack Query, и staleTime / gcTime напрямую влияют на количество RPC запросов:
const config = createConfig({
// ...
// Настройка QueryClient:
})
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 12_000, // данные свежие 12 секунд — нет повторного запроса
gcTime: 5 * 60_000, // кэш живёт 5 минут
retry: 2,
retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30_000),
},
},
})
Для balance данных staleTime: 12_000 разумен — новый блок каждые ~12 секунд. Для статичных данных (token metadata, contract addresses) — staleTime: Infinity.
WebSocket вместо polling
Для real-time данных (цены, события) watchContractEvent из wagmi использует eth_subscribe через WebSocket. Это кардинально лучше polling каждые N секунд:
useWatchContractEvent({
address: poolAddress,
abi: poolAbi,
eventName: 'Swap',
onLogs: (logs) => {
// обновляем цену при каждом свапе
updatePrice(logs)
},
})
Требует WebSocket-совместимого RPC провайдера — Alchemy, QuickNode, Infura (WSS endpoint).
Bundle size и загрузка
Tree shaking viem vs ethers.js
viem спроектирован с tree shaking в уме — импортируешь только то, что используешь. ethers v5 при полном импорте добавляет ~200KB gzipped. viem при типичном использовании — ~40–60KB.
Проверяем bundle analyzer:
ANALYZE=true next build
# или
npx vite-bundle-analyzer
Типичные находки: случайный импорт всего ethers вместо конкретных функций, дублирование bigint polyfills, несколько версий одного пакета в node_modules.
Code splitting по маршрутам
Компоненты работы с кошельком — под dynamic import:
const WalletModal = dynamic(() => import('@/components/WalletModal'), {
ssr: false, // обязательно для Web3 компонентов
loading: () => <Skeleton className="h-10 w-32" />,
})
ssr: false критично — wallet adapter и wagmi обращаются к window.ethereum при инициализации.
React rendering оптимизация
Изоляция re-renders
Главный паттерн: не читать весь account объект там, где нужен только адрес.
// Плохо — компонент перерендерится при любом изменении аккаунта:
const { address, isConnected, chain, connector } = useAccount()
// Хорошо — перерендер только при изменении address:
const { address } = useAccount({ select: (a) => a.address })
select в wagmi хуках работает как selector в Redux — мемоизирует производное значение.
Memoization тяжёлых вычислений
// Форматирование большого списка позиций
const formattedPositions = useMemo(() =>
rawPositions.map(pos => ({
...pos,
valueUsd: formatUnits(pos.amount * pos.price, 18),
pnlPercent: ((pos.currentPrice - pos.entryPrice) / pos.entryPrice * 100).toFixed(2),
})),
[rawPositions] // только при изменении данных
)
Виртуализация длинных списков
Таблица с 1000+ транзакций без виртуализации — гарантированный лаг. @tanstack/react-virtual или react-window:
import { useVirtualizer } from '@tanstack/react-virtual'
const rowVirtualizer = useVirtualizer({
count: transactions.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 56, // высота строки
overscan: 5,
})
Метрики и профилирование
Перед оптимизацией — измерения. React DevTools Profiler показывает какие компоненты рендерятся и сколько. Chrome DevTools Network tab — сколько RPC запросов и их timing. Lighthouse для общих метрик загрузки (LCP, TTI).
Типичные результаты после оптимизации: количество RPC запросов уменьшается в 5–10x через multicall батчинг, TTI снижается на 30–50% через code splitting, re-render бюджет уменьшается в 2–3x через правильные selectors.
Работа занимает 3–5 дней: аудит текущих узких мест, настройка multicall, TanStack Query параметров, bundle анализ и устранение лишних зависимостей, профилирование React rendering.







