Lazy loading keeps your initial bundle small and loads code only when needed. Here are production patterns.
Dynamic Component Import
import dynamic from "next/dynamic";
// Basic dynamic import with loading state
const HeavyChart = dynamic(() => import("@/components/HeavyChart"), {
loading: () => (
<div className="h-64 bg-muted animate-pulse rounded-lg" />
),
});
// Skip SSR for client-only components
const MapComponent = dynamic(() => import("@/components/Map"), {
ssr: false,
loading: () => (
<div className="h-96 bg-muted animate-pulse rounded-lg flex items-center justify-center">
<span className="text-muted-foreground text-sm">Loading map...</span>
</div>
),
});
Intersection Observer Lazy Loading
"use client";
import { useRef, useState, useEffect, type ComponentType } from "react";
interface LazyComponentProps<P> {
loader: () => Promise<{ default: ComponentType<P> }>;
props: P;
rootMargin?: string;
placeholder?: React.ReactNode;
}
export function LazyComponent<P extends Record<string, unknown>>({
loader,
props,
rootMargin = "200px",
placeholder,
}: LazyComponentProps<P>) {
const ref = useRef<HTMLDivElement>(null);
const [Component, setComponent] = useState<ComponentType<P> | null>(null);
useEffect(() => {
const element = ref.current;
if (!element) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
loader().then((mod) => setComponent(() => mod.default));
observer.disconnect();
}
},
{ rootMargin },
);
observer.observe(element);
return () => observer.disconnect();
}, [loader, rootMargin]);
if (Component) return <Component {...props} />;
return (
<div ref={ref}>
{placeholder ?? (
<div className="h-48 bg-muted animate-pulse rounded-lg" />
)}
</div>
);
}
Usage:
<LazyComponent
loader={() => import("@/components/HeavyChart")}
props={{ data: chartData }}
rootMargin="400px"
/>
Conditional Feature Loading
"use client";
import { useState, useCallback, type ComponentType } from "react";
type LazyModule<P> = { default: ComponentType<P> };
export function useConditionalModule<P>() {
const [Module, setModule] = useState<ComponentType<P> | null>(null);
const [loading, setLoading] = useState(false);
const load = useCallback(async (loader: () => Promise<LazyModule<P>>) => {
setLoading(true);
try {
const mod = await loader();
setModule(() => mod.default);
} finally {
setLoading(false);
}
}, []);
return { Module, loading, load };
}
// Usage
function AdminPanel() {
const { Module: AdvancedEditor, loading, load } = useConditionalModule();
return (
<div>
<button
onClick={() => load(() => import("@/components/AdvancedEditor"))}
disabled={loading}
className="px-4 py-2 bg-primary text-primary-foreground rounded-md"
>
{loading ? "Loading..." : "Open Advanced Editor"}
</button>
{AdvancedEditor && <AdvancedEditor />}
</div>
);
}
Route-Based Preloading
"use client";
import Link from "next/link";
import { useCallback } from "react";
// Preload a module when the user hovers
const moduleCache = new Map<string, Promise<unknown>>();
function preloadModule(path: string) {
if (moduleCache.has(path)) return;
const promise = import(/* webpackPrefetch: true */ `@/components/${path}`);
moduleCache.set(path, promise);
}
export function PreloadLink({
href,
preload,
children,
className,
}: {
href: string;
preload: string;
children: React.ReactNode;
className?: string;
}) {
const handleMouseEnter = useCallback(() => {
preloadModule(preload);
}, [preload]);
return (
<Link
href={href}
className={className}
onMouseEnter={handleMouseEnter}
onFocus={handleMouseEnter}
>
{children}
</Link>
);
}
Heavy Library Loading
"use client";
import { useCallback, useState, useRef } from "react";
// Load heavy libraries on demand
export function PdfGenerator() {
const [generating, setGenerating] = useState(false);
const pdfLibRef = useRef<typeof import("@react-pdf/renderer") | null>(null);
const generatePdf = useCallback(async () => {
setGenerating(true);
try {
// Only load the library when needed
if (!pdfLibRef.current) {
pdfLibRef.current = await import("@react-pdf/renderer");
}
const { pdf, Document, Page, Text } = pdfLibRef.current;
const doc = (
<Document>
<Page>
<Text>Generated PDF content</Text>
</Page>
</Document>
);
const blob = await pdf(doc).toBlob();
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "document.pdf";
link.click();
URL.revokeObjectURL(url);
} finally {
setGenerating(false);
}
}, []);
return (
<button
onClick={generatePdf}
disabled={generating}
className="px-4 py-2 bg-primary text-primary-foreground rounded-md disabled:opacity-50"
>
{generating ? "Generating..." : "Download PDF"}
</button>
);
}
Idle Loading Pattern
"use client";
import { useEffect, useState, type ComponentType } from "react";
export function useIdleLoad<P>(loader: () => Promise<{ default: ComponentType<P> }>) {
const [Component, setComponent] = useState<ComponentType<P> | null>(null);
useEffect(() => {
if ("requestIdleCallback" in window) {
const id = requestIdleCallback(() => {
loader().then((mod) => setComponent(() => mod.default));
});
return () => cancelIdleCallback(id);
}
// Fallback for Safari
const timeout = setTimeout(() => {
loader().then((mod) => setComponent(() => mod.default));
}, 2000);
return () => clearTimeout(timeout);
}, [loader]);
return Component;
}
Need Performance Optimization?
We optimize web applications for speed with code splitting and lazy loading. Contact us to improve your site.