How to Improve Performance in React and Next.js Dashboards
Fix slow dashboards with memoization, virtualization, lazy-loaded charts, API caching, and bundle optimization. Practical checklist for React and Next.js performance.
Why dashboards get slow
Dashboards aggregate data from multiple sources, render complex charts, and update frequently. Performance degrades from unnecessary re-renders, unvirtualized large lists, eager chart library imports, missing cache configuration, and layout shifts during data loading. Each issue alone is minor. Combined, they make dashboards feel broken.
Avoiding unnecessary renders
The biggest performance win in React dashboards is preventing re-renders that do not change the UI. Split components so wallet state changes do not re-render chart components.
// Bad: entire dashboard re-renders on wallet events
function Dashboard() {
const { address } = useAccount();
const { data: portfolio } = usePortfolio(address);
const { data: tvl } = useProtocolTVL();
return <div>{/* everything here re-renders */}</div>;
}
// Good: isolated subscriptions
function Dashboard() {
return (
<div>
<WalletHeader />
<ProtocolTVL />
<PortfolioPanel />
</div>
);
}Use React Query's select to subscribe to derived data only:
const { data: totalUsd } = useQuery({
queryKey: ['portfolio', address],
queryFn: () => fetchPortfolio(address),
select: (data) => data.positions.reduce((sum, p) => sum + p.usdValue, 0),
});Memoization basics
Memoize expensive computations and stable callback references. Do not memoize everything — profile first.
const chartData = useMemo(
() => rawData.map((d) => ({ date: formatDate(d.timestamp), value: d.amount / 1e18 })),
[rawData]
);
const handleSort = useCallback((column: string) => {
setSortColumn(column);
}, []);Use React.memo for pure presentation components that receive frequently changing parent props:
export const TokenRow = memo(function TokenRow({ token }: { token: TokenPosition }) {
return (
<tr>
<td>{token.symbol}</td>
<td>{formatUsd(token.usdValue)}</td>
</tr>
);
});Pagination and virtualization
Never render 1,000 table rows in the DOM. Use server-side pagination for data fetching and client-side virtualization for rendering.
import { useVirtualizer } from '@tanstack/react-virtual';
function VirtualTable({ rows }: { rows: Transaction[] }) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: rows.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 48,
overscan: 10,
});
return (
<div ref={parentRef} className="h-[600px] overflow-auto">
<div style={{ height: virtualizer.getTotalSize() }}>
{virtualizer.getVirtualItems().map((item) => (
<div key={item.key} style={{ height: item.size, transform: `translateY(${item.start}px)` }}>
<TransactionRow tx={rows[item.index]} />
</div>
))}
</div>
</div>
);
}Lazy loading charts
Chart libraries are heavy (200KB+). Load them only when needed.
import dynamic from 'next/dynamic';
const TVLChart = dynamic(() => import('@/components/charts/TVLChart'), {
loading: () => <div className="h-[300px] animate-pulse rounded-xl bg-white/5" />,
ssr: false,
});API caching
Configure TanStack Query with appropriate staleTime per data type. Use Next.js Route Handler caching for server-side API responses.
// app/api/tvl/route.ts
export async function GET() {
const data = await fetchTVLFromDeFiLlama();
return Response.json(data, {
headers: { 'Cache-Control': 's-maxage=120, stale-while-revalidate=300' },
});
}Image and font optimization
Use next/image for all images with explicit width and height. Use next/font for custom fonts to eliminate layout shift from font loading.
import Image from 'next/image';
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'], display: 'swap' });Bundle size improvements
Analyze with @next/bundle-analyzer. Tree-shake chart libraries (import specific components, not entire library). Replace moment.js with date-fns or Intl.DateTimeFormat. Check for duplicate dependencies with npm ls.
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
module.exports = withBundleAnalyzer({});Performance checklist
Profile with React DevTools Profiler before optimizing. Split wallet and data subscriptions. Configure staleTime per query. Virtualize tables over 100 rows. Lazy-load chart libraries. Use next/image and next/font. Edge-cache public API routes. Set fixed heights on chart containers. Distinguish isLoading from isFetching in UI. Run Lighthouse on key pages before shipping.
FAQ
**Should I use React.memo everywhere?** No. Profile first. Memoize list items and expensive chart components. Skip it for simple components.
**How do I measure dashboard performance?** React DevTools Profiler for render times. Lighthouse for page load. Web Vitals in production via Vercel Analytics or web-vitals library.
**Is SWR or React Query faster?** Performance is similar. TanStack Query has better devtools and wagmi integration.
**When should I reach for Web Workers?** Only for heavy client-side computation (large dataset aggregation, cryptography). Most dashboard slowness is render-related, not compute-related.
Want to work together? I build Web3 dashboards and DeFi interfaces.