Next.js Lazy Loading Cheat Sheet

Next.js Lazy Loading Cheatsheet

Dynamic Imports

Basic Dynamic Import

import dynamic from 'next/dynamic';

const DynamicComponent = dynamic(() => import('./HeavyComponent'), {
    loading: () => <p>Loading...</p>,
    ssr: false // Disable server-side rendering
});

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>
    );
}

Code Splitting Strategies

Route-Based Splitting

// app/dashboard/page.js
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

// app/components/FeatureComponents.js
import dynamic from 'next/dynamic';

// Lazy load feature components
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>
});

// Usage
import { VideoPlayer, PDFViewer, MapComponent } from './FeatureComponents';

Library-Based Splitting

// Heavy libraries
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
});

Image Lazy Loading

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>
    );
}

Component Lazy Loading Patterns

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>
    );
}

Data Lazy Loading

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>
    );
}

Performance Optimization

Preloading Critical Components

import dynamic from 'next/dynamic';

// Preload critical components
const CriticalComponent = dynamic(() => import('./CriticalComponent'), {
    loading: () => <div>Loading critical component...</div>
});

// Lazy load non-critical components
const NonCriticalComponent = dynamic(() => import('./NonCriticalComponent'), {
    loading: () => <div>Loading...</div>,
    ssr: false
});

// Preload on user interaction
const PreloadableComponent = dynamic(() => import('./PreloadableComponent'), {
    loading: () => <div>Loading...</div>
});

export default function App() {
    const handleHover = () => {
        // Preload component on hover
        import('./PreloadableComponent');
    };

    return (
        <div>
            <CriticalComponent />
            <div onMouseEnter={handleHover}>
                <NonCriticalComponent />
            </div>
        </div>
    );
}

Bundle Analysis

// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
    enabled: process.env.ANALYZE === 'true',
});

module.exports = withBundleAnalyzer({
    // your existing config
});

// Run: ANALYZE=true npm run build

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>
    );
}

Best Practices

When to Use Lazy Loading

// ✅ Good candidates for lazy loading:
// - Heavy third-party libraries (charts, maps, editors)
// - Non-critical UI components (modals, tooltips)
// - Below-the-fold content
// - Route-based components

// ❌ Avoid lazy loading:
// - Critical above-the-fold components
// - Small, frequently used components
// - Components needed for initial render

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 () => {
            // Cleanup when component unmounts
            if (componentRef.current) {
                componentRef.current.cleanup();
            }
        };
    }, []);

    return (
        <div>
            <MemoryIntensiveComponent ref={componentRef} />
        </div>
    );
}