Skip to content

Performance Best Practices

Performance Best Practices

Code Splitting

Use dynamic imports for large components:

import dynamic from 'next/dynamic';

// Lazy load heavy components
const Chart = dynamic(() => import('@/components/Chart'), {
  loading: () => <ChartSkeleton />,
  ssr: false, // Skip SSR for client-only components
});

// Route-based splitting (automatic in Next.js App Router)
// Each page is automatically code-split

Memoization

Use memoization judiciously:

// React.memo - prevent re-renders from parent
const ExpensiveList = memo(function ExpensiveList({ items }: Props) {
  return items.map(item => <ExpensiveItem key={item.id} item={item} />);
});

// useMemo - cache expensive calculations
function Dashboard({ data }: { data: SalesData[] }) {
  const totals = useMemo(() => {
    return data.reduce((acc, item) => ({
      revenue: acc.revenue + item.revenue,
      orders: acc.orders + item.orders,
    }), { revenue: 0, orders: 0 });
  }, [data]);
}

// useCallback - stable function references
function Parent() {
  const handleClick = useCallback((id: string) => {
    // ...
  }, []);

  return <Child onClick={handleClick} />;
}

Don't over-memoize: Memoization has overhead. Only use it when: - Component re-renders frequently with same props - Calculations are genuinely expensive - Callbacks are passed to memoized children

Image Optimization

Always use SafeImage (aliased as Image) with the appropriate image preset — never import raw next/image:

import Image from "@/components/Image/SafeImage";
import { IMAGE_PRESETS } from "@/constants/image-presets";

<Image
  src={getServerFileUrl(product.imagePath)}
  alt="Product"
  {...IMAGE_PRESETS.lg}
  priority // For above-the-fold images
  placeholder="blur"
  blurDataURL={rgbDataURL(235, 235, 235)}
/>

See the Images guide for full details on rendering primitives, presets, and the download proxy.

Lazy Loading

Defer loading of off-screen content:

// Intersection Observer for custom lazy loading
function LazySection({ children }: { children: React.ReactNode }) {
  const [isVisible, setIsVisible] = useState(false);
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setIsVisible(true);
          observer.disconnect();
        }
      },
      { rootMargin: '100px' }
    );

    if (ref.current) observer.observe(ref.current);
    return () => observer.disconnect();
  }, []);

  return (
    <div ref={ref}>
      {isVisible ? children : <Placeholder />}
    </div>
  );
}

See Also