Skip to main content

Next.js Lazy Loading

Introduction

Lazy loading is a performance optimization technique that delays loading non-critical resources until they're actually needed. In the context of Next.js applications, lazy loading allows you to split your code into smaller chunks and load components only when they're required. This approach can significantly improve the initial loading time of your application, reduce bandwidth usage, and enhance the overall user experience.

Next.js provides several built-in mechanisms for implementing lazy loading:

  1. Component-level lazy loading with dynamic imports
  2. Image lazy loading with the next/image component
  3. Font optimization with next/font
  4. Route-based code splitting (automatic in Next.js)

In this guide, we'll explore each of these approaches and learn how to implement them effectively in your Next.js applications.

Component-Level Lazy Loading

Using dynamic imports

Next.js provides the dynamic function from the next/dynamic module that allows you to dynamically import components. This is equivalent to using React's lazy and Suspense features but is specifically optimized for Next.js.

Basic Usage

jsx
import dynamic from 'next/dynamic';

// Instead of a regular import like:
// import HeavyComponent from '../components/HeavyComponent';

// Use dynamic import:
const HeavyComponent = dynamic(() => import('../components/HeavyComponent'));

function HomePage() {
return (
<div>
<h1>Welcome to my website</h1>
<HeavyComponent />
</div>
);
}

export default HomePage;

When a user visits this page, the main content will load first, and the HeavyComponent will be loaded separately only when needed.

Adding a Loading State

You can show a loading indicator while the component is being loaded:

jsx
const HeavyComponent = dynamic(() => import('../components/HeavyComponent'), {
loading: () => <p>Loading component...</p>,
});

Disabling Server-Side Rendering

For components that rely on browser-specific APIs, you might want to disable server-side rendering:

jsx
const ClientOnlyComponent = dynamic(() => import('../components/ClientOnlyComponent'), {
ssr: false,
loading: () => <p>Loading component...</p>,
});

Image Lazy Loading

Next.js provides the next/image component which automatically implements lazy loading for images. The component only loads images when they enter the viewport, greatly improving page performance.

Basic Usage

jsx
import Image from 'next/image';

function ProductPage() {
return (
<div>
<h1>Featured Product</h1>
<Image
src="/images/product.jpg"
alt="Product image"
width={500}
height={300}
priority={false} // Set to true for above-the-fold images
/>
</div>
);
}

The Image component automatically:

  • Lazy loads images that are not in the viewport
  • Optimizes image size based on the device
  • Prevents layout shifts by reserving the space with the specified dimensions

Priority Images

For key images that are visible immediately when the page loads (above the fold), you should set the priority attribute to true to preload them:

jsx
<Image
src="/images/hero-image.jpg"
alt="Hero image"
width={1200}
height={600}
priority={true}
/>

Placeholder Images

You can add a blur placeholder while the image loads:

jsx
<Image
src="/images/product.jpg"
alt="Product image"
width={500}
height={300}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAoHBwgHBgoICAgLCgoLDhgQDg0NDh0VFhEYIx8lJCIfIiEmKzcvJik0KSEiMEExNDk7Pj4+JS5ESUM8SDc9Pjv/2wBDAQoLCw4NDhwQEBw7KCIoOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozv/wAARCAAIAAoDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAb/xAAhEAACAQMDBQAAAAAAAAAAAAABAgMABAUGIWEREiMxUf/EABUBAQEAAAAAAAAAAAAAAAAAAAMF/8QAGhEAAgIDAAAAAAAAAAAAAAAAAAECEgMRkf/aAAwDAQACEQMRAD8AltJagyeH0AthI5xdrLcNM91BF5pX2HautoooTnWycq//2Q=="
/>

Font Optimization with next/font

Next.js 13 introduced a new font system that automatically optimizes fonts by downloading them at build time and serving them with your application. This eliminates layout shifts caused by font loading.

Using Google Fonts

jsx
import { Inter } from 'next/font/google';

// Initialize the font object
const inter = Inter({
subsets: ['latin'],
display: 'swap',
});

function MyApp({ Component, pageProps }) {
return (
<main className={inter.className}>
<Component {...pageProps} />
</main>
);
}

Using Local Fonts

jsx
import localFont from 'next/font/local';

// Load a local font file
const myFont = localFont({
src: './fonts/my-font.woff2',
display: 'swap',
});

function MyComponent() {
return (
<div className={myFont.className}>
This text will use the custom font.
</div>
);
}

Route-Based Code Splitting

Next.js automatically code-splits your application at the page level. This means when a user navigates to a specific page, only the code for that page is loaded.

For example, if you have these files:

  • pages/index.js
  • pages/about.js
  • pages/contact.js

Next.js will create separate chunks for each page, and they'll only be loaded when a user navigates to that specific route.

Prefetching Routes

Next.js automatically prefetches linked pages that appear in the viewport. This means that by the time the user clicks a link, the destination page may already be loaded in the background:

jsx
import Link from 'next/link';

function Navbar() {
return (
<nav>
<Link href="/">Home</Link>
<Link href="/about">About</Link>
<Link href="/contact">Contact</Link>
</nav>
);
}

When these links are visible in the viewport, Next.js will automatically prefetch the linked pages in the background.

Real-World Example: E-commerce Product List

Let's build a practical example of a product list page that uses lazy loading techniques for better performance:

jsx
import { useState } from 'react';
import Image from 'next/image';
import dynamic from 'next/dynamic';

// Lazy load the product details modal
const ProductDetailModal = dynamic(() => import('../components/ProductDetailModal'), {
loading: () => <div className="modal-placeholder">Loading details...</div>,
ssr: false, // Since modals are client-side only
});

export default function ProductListPage({ products }) {
const [selectedProduct, setSelectedProduct] = useState(null);

return (
<div className="product-grid">
{products.map((product) => (
<div key={product.id} className="product-card">
<Image
src={product.imageUrl}
alt={product.name}
width={300}
height={200}
// Only prioritize first few images that would be above the fold
priority={product.id <= 4}
/>
<h3>{product.name}</h3>
<p>${product.price}</p>
<button onClick={() => setSelectedProduct(product)}>
View Details
</button>
</div>
))}

{/* Modal only gets loaded when a product is selected */}
{selectedProduct && (
<ProductDetailModal
product={selectedProduct}
onClose={() => setSelectedProduct(null)}
/>
)}
</div>
);
}

// This function would fetch your products from an API or database
export async function getStaticProps() {
// Fetch products from your API
const res = await fetch('https://api.example.com/products');
const products = await res.json();

return {
props: {
products,
},
revalidate: 60, // Re-generate page every 60 seconds
};
}

In this example:

  1. The product grid loads with optimized images
  2. Only the first few "above the fold" images are prioritized
  3. The product detail modal is lazy loaded only when needed
  4. The page is statically generated with incremental static regeneration

Summary

Lazy loading is a powerful optimization technique in Next.js applications that can significantly improve performance by:

  • Loading components only when they're needed using dynamic imports
  • Automatically lazy loading images that are not in the viewport
  • Optimizing fonts to prevent layout shifts
  • Leveraging automatic code splitting at the page level

By implementing these techniques, you can create faster, more efficient Next.js applications that provide a better user experience, reduce bounce rates, and potentially improve SEO rankings.

Additional Resources

Exercises

  1. Convert an existing component in your project to use lazy loading with dynamic.
  2. Implement the next/image component for all images on a page and test the performance improvements.
  3. Create a tabbed interface where each tab's content is lazy loaded when the tab is clicked.
  4. Add blur placeholders to improve the user experience while images load.
  5. Use Chrome DevTools' Network tab to measure the performance improvements after implementing lazy loading in your application.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)