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¶
- Frontend Profiling -- Techniques for diagnosing React rendering and bundle size issues