← Back to articles
April 202611 min read

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.

PerformanceReactNext.jsOptimization

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.