}>
A more polished fallback improves perceived performance:
function LoadingSpinner() {
return (
);
}
}>
How to Handle Errors with Error Boundaries
React.lazy() and Suspense don’t handle loading errors (for example, network failures or missing chunks). For that, you need an Error Boundary.
Error Boundaries are class components that use componentDidCatch or static getDerivedStateFromError to catch errors in their child tree and render a fallback UI.
Here is a simple Error Boundary:
import { Component } from 'react';
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return this.props.fallback || Something went wrong.
;
}
return this.props.children;
}
}
Wrap your Suspense boundary with an Error Boundary:
import { lazy, Suspense } from 'react';
import ErrorBoundary from './ErrorBoundary';
const HeavyChart = lazy(() => import('./HeavyChart'));
function App() {
return (
Failed to load chart. Please try again.}>
Loading chart...}>
);
}
If the chunk fails to load, the Error Boundary catches it and shows your fallback instead of a blank screen or unhandled error.
How to Use next/dynamic in Next.js
Next.js provides next/dynamic, which wraps React.lazy() and Suspense and adds options tailored for Next.js (including Server-Side Rendering).
Basic usage:
'use client';
import dynamic from 'next/dynamic';
const ComponentA = dynamic(() => import('../components/A'));
const ComponentB = dynamic(() => import('../components/B'));
export default function Page() {
return (
);
}
Custom Loading UI
Use the loading option to show a placeholder while the component loads:
const HeavyChart = dynamic(() => import('../components/HeavyChart'), {
loading: () => Loading chart...
,
});
Disable Server-Side Rendering
For components that must run only on the client (for example, those using window or browser-only APIs), set ssr: false:
const ClientOnlyMap = dynamic(() => import('../components/Map'), {
ssr: false,
loading: () => Loading map...
,
});
Note: ssr: false works only for Client Components. Use it inside a 'use client' file.
Load on Demand
You can load a component only when a condition is met:
'use client';
import { useState } from 'react';
import dynamic from 'next/dynamic';
const Modal = dynamic(() => import('../components/Modal'), {
loading: () => Opening modal...
,
});
export default function Page() {
const [showModal, setShowModal] = useState(false);
return (
{showModal && setShowModal(false)} />}
);
}
Named Exports
For named exports, return the component from the dynamic import:
const Hello = dynamic(() =>
import('../components/hello').then((mod) => mod.Hello)
);
Using Suspense with next/dynamic
In React 18+, you can use suspense: true to rely on a parent Suspense boundary instead of the loading option:
const HeavyChart = dynamic(() => import('../components/HeavyChart'), {
suspense: true,
});
// In your component:
Loading...}>
Important: When using suspense: true, you can’t use ssr: false or the loading option. Use the Suspense fallback instead.
React.lazy vs next/dynamic: When to Use Each
| Feature | React.lazy + Suspense | next/dynamic |
|---|---|---|
| Framework | Any React app (Create React App, Vite, etc.) | Next.js only |
| Server-Side Rendering | Not supported | Supported by default |
| Disable SSR | N/A | ssr: false option |
| Loading UI | Suspense fallback prop |
Built-in loading option |
| Error handling | Requires Error Boundary | Requires Error Boundary |
| Named exports | Manual .then() mapping |
Same .then() pattern |
| Suspense mode | Always uses Suspense | Optional via suspense: true |
When to Use React.lazy
-
You’re building a pure React app (no Next.js)
-
You use Create React App, Vite, or a custom Webpack setup
-
You don’t need Server-Side Rendering
-
You want a simple, framework-agnostic approach
When to Use next/dynamic
-
You’re building a Next.js app
-
You need SSR for some components and want to disable it for others
-
You want built-in loading placeholders without manually adding
Suspense -
You want Next.js-specific optimizations and defaults
Real-World Examples
Example 1: Route-Based Code Splitting in React
Split your app by route so each page loads only when the user navigates to it:
// App.jsx
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import ErrorBoundary from './ErrorBoundary';
const Home = lazy(() => import('./pages/Home'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
function App() {
return (
Failed to load page.}>
Loading page...}>
} />
} />
} />
);
}
Example 2: Lazy Loading a Heavy Chart Library in Next.js
Defer loading a chart library until the user opens the analytics section:
// app/analytics/page.jsx
'use client';
import { useState } from 'react';
import dynamic from 'next/dynamic';
const Chart = dynamic(() => import('../components/Chart'), {
ssr: false,
loading: () => (
),
});
export default function AnalyticsPage() {
const [showChart, setShowChart] = useState(false);
return (
{showChart && }
);
}
Example 3: Lazy Loading a Modal
Load a modal component only when the user clicks to open it:
// React (with React.lazy)
import { lazy, Suspense, useState } from 'react';
const Modal = lazy(() => import('./Modal'));
function ProductPage() {
const [showModal, setShowModal] = useState(false);
return (
{showModal && (
setShowModal(false)} />
)}
);
}
// Next.js (with next/dynamic)
'use client';
import { useState } from 'react';
import dynamic from 'next/dynamic';
const Modal = dynamic(() => import('./Modal'), {
loading: () => null,
});
export default function ProductPage() {
const [showModal, setShowModal] = useState(false);
return (
{showModal && setShowModal(false)} />}
);
}
Example 4: Lazy Loading External Libraries
Load a library only when the user needs it (for example, when they start typing in a search box):
'use client';
import { useState } from 'react';
const names = ['Alice', 'Bob', 'Charlie', 'Diana'];
export default function SearchPage() {
const [results, setResults] = useState([]);
const [query, setQuery] = useState('');
const handleSearch = async (value) => {
setQuery(value);
if (!value) {
setResults([]);
return;
}
// Load fuse.js only when user searches
const Fuse = (await import('fuse.js')).default;
const fuse = new Fuse(names);
setResults(fuse.search(value));
};
return (
);
}
Conclusion
Lazy loading improves performance by splitting your bundle and loading code only when needed. Here’s what you learned:
-
React.lazy() – Use in plain React apps for code splitting. It requires a default export and works with dynamic
import(). -
Suspense – Wrap lazy components in
Suspenseand provide afallbackfor the loading state. -
Error Boundaries – Use them to catch chunk load failures and show a friendly error UI.
-
next/dynamic – Use in Next.js for the same benefits plus SSR control and built-in loading options.
Choose React.lazy for React-only projects and next/dynamic for Next.js. Combine them with Suspense and Error Boundaries for a solid lazy-loading setup.
Start by identifying your heaviest components (charts, modals, admin panels) and lazy load them. Measure your bundle size and Core Web Vitals before and after to see the impact.
Learn to code for free. freeCodeCamp’s open source curriculum has helped more than 40,000 people get jobs as developers. Get started

