Next.js Lazy Loading Cheatsheet
Basic Dynamic Import
import dynamic from 'next/dynamic';
const DynamicComponent = dynamic(() => import('./HeavyComponent'), {
loading: () => <p>Loading...</p>,
ssr: false
});
export default function Page() {
return (
<div>
<h1>Main Content</h1>
<DynamicComponent />
</div>
);
}
Dynamic Import with Loading Component
import dynamic from 'next/dynamic';
const LoadingSpinner = () => (
<div style={{ padding: '20px', textAlign: 'center' }}>
<div className="spinner"></div>
<p>Loading component...</p>
</div>
);
const ChartComponent = dynamic(() => import('./ChartComponent'), {
loading: LoadingSpinner,
ssr: false
});
export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<ChartComponent />
</div>
);
}
Conditional Dynamic Import
'use client';
import { useState } from 'react';
import dynamic from 'next/dynamic';
const Modal = dynamic(() => import('./Modal'), {
loading: () => <div>Loading modal...</div>
});
export default function App() {
const [showModal, setShowModal] = useState(false);
return (
<div>
<button onClick={() => setShowModal(true)}>
Open Modal
</button>
{showModal && <Modal onClose={() => setShowModal(false)} />}
</div>
);
}
Route-Based Splitting
import dynamic from 'next/dynamic';
const DashboardChart = dynamic(() => import('@/components/DashboardChart'));
const UserList = dynamic(() => import('@/components/UserList'));
const Analytics = dynamic(() => import('@/components/Analytics'));
export default function DashboardPage() {
return (
<div>
<h1>Dashboard</h1>
<DashboardChart />
<UserList />
<Analytics />
</div>
);
}
Feature-Based Splitting
import dynamic from 'next/dynamic';
export const VideoPlayer = dynamic(() => import('./VideoPlayer'), {
ssr: false
});
export const PDFViewer = dynamic(() => import('./PDFViewer'), {
ssr: false
});
export const MapComponent = dynamic(() => import('./MapComponent'), {
ssr: false,
loading: () => <div>Loading map...</div>
});
import { VideoPlayer, PDFViewer, MapComponent } from './FeatureComponents';
Library-Based Splitting
const Chart = dynamic(() => import('react-chartjs-2'), {
ssr: false,
loading: () => <div>Loading chart library...</div>
});
const MonacoEditor = dynamic(() => import('@monaco-editor/react'), {
ssr: false,
loading: () => <div>Loading editor...</div>
});
const ThreeJS = dynamic(() => import('three'), {
ssr: false
});
Basic Image Lazy Loading
import Image from 'next/image';
export default function Gallery({ images }) {
return (
<div>
{images.map((image, index) => (
<Image
key={image.id}
src={image.src}
alt={image.alt}
width={300}
height={200}
loading={index < 3 ? 'eager' : 'lazy'} // Lazy load after first 3
/>
))}
</div>
);
}
Intersection Observer for Custom Lazy Loading
'use client';
import { useState, useEffect, useRef } from 'react';
import Image from 'next/image';
export default function LazyImage({ src, alt, width, height }) {
const [isVisible, setIsVisible] = useState(false);
const [imageSrc, setImageSrc] = useState(null);
const imgRef = useRef();
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
setImageSrc(src);
observer.unobserve(imgRef.current);
}
},
{ threshold: 0.1 }
);
if (imgRef.current) {
observer.observe(imgRef.current);
}
return () => observer.disconnect();
}, [src]);
return (
<div ref={imgRef}>
{isVisible && imageSrc ? (
<Image
src={imageSrc}
alt={alt}
width={width}
height={height}
/>
) : (
<div
style={{
width,
height,
backgroundColor: '#f0f0f0',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
Loading...
</div>
)}
</div>
);
}
Modal/Popup Lazy Loading
'use client';
import { useState } from 'react';
import dynamic from 'next/dynamic';
const ContactForm = dynamic(() => import('./ContactForm'), {
loading: () => <div>Loading form...</div>
});
const ImageGallery = dynamic(() => import('./ImageGallery'), {
loading: () => <div>Loading gallery...</div>
});
export default function App() {
const [activeModal, setActiveModal] = useState(null);
return (
<div>
<button onClick={() => setActiveModal('contact')}>
Contact Us
</button>
<button onClick={() => setActiveModal('gallery')}>
View Gallery
</button>
{activeModal === 'contact' && (
<ContactForm onClose={() => setActiveModal(null)} />
)}
{activeModal === 'gallery' && (
<ImageGallery onClose={() => setActiveModal(null)} />
)}
</div>
);
}
Tab Content Lazy Loading
'use client';
import { useState } from 'react';
import dynamic from 'next/dynamic';
const Tab1Content = dynamic(() => import('./Tab1Content'));
const Tab2Content = dynamic(() => import('./Tab2Content'));
const Tab3Content = dynamic(() => import('./Tab3Content'));
export default function TabbedInterface() {
const [activeTab, setActiveTab] = useState(0);
const tabs = [
{ id: 0, label: 'Overview', component: Tab1Content },
{ id: 1, label: 'Details', component: Tab2Content },
{ id: 2, label: 'Settings', component: Tab3Content }
];
const ActiveComponent = tabs[activeTab].component;
return (
<div>
<div>
{tabs.map(tab => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={activeTab === tab.id ? 'active' : ''}
>
{tab.label}
</button>
))}
</div>
<div>
<ActiveComponent />
</div>
</div>
);
}
Infinite Scroll
'use client';
import { useState, useEffect, useRef } from 'react';
export default function InfiniteScroll({ fetchData }) {
const [items, setItems] = useState([]);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const [hasMore, setHasMore] = useState(true);
const observerRef = useRef();
const lastItemRef = useRef();
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && hasMore && !loading) {
loadMore();
}
},
{ threshold: 0.1 }
);
if (lastItemRef.current) {
observer.observe(lastItemRef.current);
}
observerRef.current = observer;
return () => observer.disconnect();
}, [hasMore, loading]);
const loadMore = async () => {
setLoading(true);
try {
const newItems = await fetchData(page);
if (newItems.length === 0) {
setHasMore(false);
} else {
setItems(prev => [...prev, ...newItems]);
setPage(prev => prev + 1);
}
} catch (error) {
console.error('Error loading more items:', error);
} finally {
setLoading(false);
}
};
return (
<div>
{items.map((item, index) => (
<div
key={item.id}
ref={index === items.length - 1 ? lastItemRef : null}
>
{item.content}
</div>
))}
{loading && <div>Loading more...</div>}
{!hasMore && <div>No more items</div>}
</div>
);
}
Virtual Scrolling
'use client';
import { useState, useEffect, useRef } from 'react';
export default function VirtualList({ items, itemHeight = 50, containerHeight = 400 }) {
const [scrollTop, setScrollTop] = useState(0);
const containerRef = useRef();
const visibleItemCount = Math.ceil(containerHeight / itemHeight);
const startIndex = Math.floor(scrollTop / itemHeight);
const endIndex = Math.min(startIndex + visibleItemCount + 1, items.length);
const visibleItems = items.slice(startIndex, endIndex);
const totalHeight = items.length * itemHeight;
const offsetY = startIndex * itemHeight;
const handleScroll = (e) => {
setScrollTop(e.target.scrollTop);
};
return (
<div
ref={containerRef}
style={{
height: containerHeight,
overflow: 'auto',
border: '1px solid #ccc'
}}
onScroll={handleScroll}
>
<div style={{ height: totalHeight, position: 'relative' }}>
<div style={{ transform: `translateY(${offsetY}px)` }}>
{visibleItems.map((item, index) => (
<div
key={startIndex + index}
style={{
height: itemHeight,
borderBottom: '1px solid #eee',
padding: '10px'
}}
>
{item.content}
</div>
))}
</div>
</div>
</div>
);
}
Preloading Critical Components
import dynamic from 'next/dynamic';
const CriticalComponent = dynamic(() => import('./CriticalComponent'), {
loading: () => <div>Loading critical component...</div>
});
const NonCriticalComponent = dynamic(() => import('./NonCriticalComponent'), {
loading: () => <div>Loading...</div>,
ssr: false
});
const PreloadableComponent = dynamic(() => import('./PreloadableComponent'), {
loading: () => <div>Loading...</div>
});
export default function App() {
const handleHover = () => {
import('./PreloadableComponent');
};
return (
<div>
<CriticalComponent />
<div onMouseEnter={handleHover}>
<NonCriticalComponent />
</div>
</div>
);
}
Bundle Analysis
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
module.exports = withBundleAnalyzer({
});
Dynamic Import with Error Boundary
'use client';
import { Component } from 'react';
import dynamic from 'next/dynamic';
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return <div>Something went wrong. Please try again.</div>;
}
return this.props.children;
}
}
const LazyComponent = dynamic(() => import('./LazyComponent'), {
loading: () => <div>Loading...</div>
});
export default function App() {
return (
<ErrorBoundary>
<LazyComponent />
</ErrorBoundary>
);
}
When to Use Lazy Loading
Loading State Management
'use client';
import { useState } from 'react';
import dynamic from 'next/dynamic';
const HeavyComponent = dynamic(() => import('./HeavyComponent'), {
loading: () => <div>Loading...</div>
});
export default function App() {
const [isLoaded, setIsLoaded] = useState(false);
return (
<div>
{!isLoaded && <div>Preparing component...</div>}
<HeavyComponent onLoad={() => setIsLoaded(true)} />
</div>
);
}
Memory Management
'use client';
import { useEffect, useRef } from 'react';
import dynamic from 'next/dynamic';
const MemoryIntensiveComponent = dynamic(() => import('./MemoryIntensiveComponent'), {
ssr: false
});
export default function App() {
const componentRef = useRef();
useEffect(() => {
return () => {
if (componentRef.current) {
componentRef.current.cleanup();
}
};
}, []);
return (
<div>
<MemoryIntensiveComponent ref={componentRef} />
</div>
);
}