Lazy loading defers loading resources until they are needed, reducing initial bundle size and improving page speed.
Pattern 1: Lazy Component Loading
import { lazy, Suspense } from "react";
// Only loads when rendered
const HeavyChart = lazy(() => import("@/components/HeavyChart"));
const CodeEditor = lazy(() => import("@/components/CodeEditor"));
export function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<ChartSkeleton />}>
<HeavyChart data={chartData} />
</Suspense>
<Suspense fallback={<EditorSkeleton />}>
<CodeEditor />
</Suspense>
</div>
);
}
function ChartSkeleton() {
return (
<div className="h-64 animate-pulse rounded-xl bg-gray-200 dark:bg-gray-800" />
);
}
function EditorSkeleton() {
return (
<div className="h-96 animate-pulse rounded-xl bg-gray-200 dark:bg-gray-800" />
);
}
Pattern 2: Next.js Dynamic Imports
import dynamic from "next/dynamic";
const Map = dynamic(() => import("@/components/Map"), {
loading: () => <div className="h-96 animate-pulse rounded-xl bg-gray-200" />,
ssr: false, // Don't render on server (browser-only APIs)
});
const RichTextEditor = dynamic(() => import("@/components/RichTextEditor"), {
ssr: false,
});
export function LocationPage() {
return (
<div>
<h1>Our Location</h1>
<Map lat={40.7128} lng={-74.006} />
</div>
);
}
Pattern 3: Intersection Observer (Load on Scroll)
"use client";
import { useRef, useState, useEffect, type ReactNode } from "react";
export function LazySection({
children,
fallback,
rootMargin = "200px",
}: {
children: ReactNode;
fallback?: ReactNode;
rootMargin?: string;
}) {
const ref = useRef<HTMLDivElement>(null);
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
const el = ref.current;
if (!el) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.disconnect();
}
},
{ rootMargin }
);
observer.observe(el);
return () => observer.disconnect();
}, [rootMargin]);
return (
<div ref={ref}>
{isVisible ? children : fallback || <div className="h-48" />}
</div>
);
}
// Usage
<LazySection fallback={<TestimonialsSkeleton />}>
<Testimonials />
</LazySection>
Pattern 4: Image Lazy Loading
Native browser support:
export function LazyImage({
src,
alt,
className,
}: {
src: string;
alt: string;
className?: string;
}) {
return (
<img
src={src}
alt={alt}
loading="lazy"
decoding="async"
className={className}
/>
);
}
With blur placeholder using Next.js Image:
import Image from "next/image";
<Image
src="/images/hero.jpg"
alt="Hero"
width={1200}
height={600}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ..."
className="rounded-xl"
/>
Pattern 5: Lazy Load Below the Fold
import dynamic from "next/dynamic";
// Above the fold - load immediately
import { Hero } from "@/components/Hero";
import { Navigation } from "@/components/Navigation";
// Below the fold - lazy load
const Features = dynamic(() => import("@/components/Features"));
const Testimonials = dynamic(() => import("@/components/Testimonials"));
const Pricing = dynamic(() => import("@/components/Pricing"));
const FAQ = dynamic(() => import("@/components/FAQ"));
const CTA = dynamic(() => import("@/components/CTA"));
export default function HomePage() {
return (
<main>
<Navigation />
<Hero />
{/* Everything below is lazy loaded */}
<Features />
<Testimonials />
<Pricing />
<FAQ />
<CTA />
</main>
);
}
Pattern 6: Lazy Data Fetching
"use client";
import { useState, useEffect, useRef } from "react";
export function LazyDataSection() {
const [data, setData] = useState<any[] | null>(null);
const [loading, setLoading] = useState(false);
const ref = useRef<HTMLDivElement>(null);
const fetched = useRef(false);
useEffect(() => {
const el = ref.current;
if (!el) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && !fetched.current) {
fetched.current = true;
setLoading(true);
fetch("/api/testimonials")
.then((res) => res.json())
.then(setData)
.finally(() => setLoading(false));
observer.disconnect();
}
},
{ rootMargin: "300px" }
);
observer.observe(el);
return () => observer.disconnect();
}, []);
return (
<section ref={ref}>
{loading && <p>Loading...</p>}
{data && (
<div className="grid grid-cols-3 gap-4">
{data.map((item) => (
<Card key={item.id} data={item} />
))}
</div>
)}
</section>
);
}
Pattern 7: Progressive Image Loading
"use client";
import { useState } from "react";
export function ProgressiveImage({
src,
placeholder,
alt,
className,
}: {
src: string;
placeholder: string;
alt: string;
className?: string;
}) {
const [loaded, setLoaded] = useState(false);
return (
<div className="relative overflow-hidden">
{/* Low-quality placeholder */}
<img
src={placeholder}
alt=""
className={`${className} ${loaded ? "opacity-0" : "opacity-100"} absolute inset-0 blur-xl transition-opacity duration-300`}
/>
{/* Full image */}
<img
src={src}
alt={alt}
loading="lazy"
onLoad={() => setLoaded(true)}
className={`${className} ${loaded ? "opacity-100" : "opacity-0"} transition-opacity duration-300`}
/>
</div>
);
}
Pattern 8: Conditional Feature Loading
"use client";
import { lazy, Suspense, useState } from "react";
const VideoPlayer = lazy(() => import("@/components/VideoPlayer"));
export function ProductSection() {
const [showVideo, setShowVideo] = useState(false);
return (
<div>
{!showVideo ? (
<button
onClick={() => setShowVideo(true)}
className="group relative aspect-video w-full overflow-hidden rounded-xl bg-gray-900"
>
<img
src="/images/video-poster.jpg"
alt="Watch demo"
className="h-full w-full object-cover opacity-80 transition group-hover:opacity-60"
/>
<div className="absolute inset-0 flex items-center justify-center">
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-white/90">
<PlayIcon className="h-6 w-6 text-gray-900" />
</div>
</div>
</button>
) : (
<Suspense fallback={<div className="aspect-video animate-pulse rounded-xl bg-gray-200" />}>
<VideoPlayer src="/videos/demo.mp4" autoPlay />
</Suspense>
)}
</div>
);
}
Need Performance Optimization?
We build fast, optimized websites and web applications using modern lazy loading, code splitting, and performance patterns. Contact us for a consultation.