// LOADING
// LOADING
// LOADING_ARTICLE
React Suspense has been in the codebase since React 16, but its full capability only landed in React 18 with the combination of concurrent rendering and streaming SSR. If you're using Next.js App Router, you're already using the infrastructure that makes this work. Here's what's actually happening and how to design your component tree to take advantage of it.
Classic SSR (or getServerSideProps in Next.js Pages Router) waterfall: the server fetches all data, renders the full page, sends the entire HTML, then the client hydrates. The user sees nothing until the slowest data fetch completes.
Streaming SSR inverts this. The server sends HTML in chunks as each part becomes ready. Fast parts appear in the browser while slower parts are still loading server-side. Suspense boundaries are how you tell React where the "loading seams" are.
A <Suspense> component wraps parts of your tree that may not be ready yet. When a suspended component is encountered during render, React shows the nearest Suspense boundary's fallback instead of blocking.
tsximport { Suspense } from 'react'; import { Skeleton } from '@/components/Skeleton'; export default function BlogPage() { return ( <div> <h1>Blog</h1> {/* Fast: shows immediately */} <Suspense fallback={<Skeleton lines={3} />}> {/* Slow: streams in when ready */} <RecentPosts /> </Suspense> <Suspense fallback={<Skeleton lines={2} />}> <TrendingTopics /> </Suspense> </div> ); }
Each <Suspense> boundary is independent. RecentPosts and TrendingTopics stream in parallel — whichever finishes first appears first.
In the App Router, async Server Components naturally integrate with Suspense. An async component that awaits data will automatically suspend its boundary while the promise resolves.
tsx// app/blog/page.tsx — Server Component async function RecentPosts() { const posts = await fetchPosts(); // suspends here while fetching return ( <ul> {posts.map(post => ( <li key={post.slug}>{post.title}</li> ))} </ul> ); } export default function Page() { return ( <Suspense fallback={<p>Loading posts...</p>}> <RecentPosts /> </Suspense> ); }
Next.js also provides a loading.tsx file convention that automatically wraps the page segment in a <Suspense> boundary. Creating app/blog/loading.tsx gives you instant fallback UI for the whole route without manually wrapping anything.
With streaming enabled, the server sends an initial HTML shell quickly — the parts outside suspended boundaries. As each suspended section resolves, React streams an additional HTML chunk containing the rendered content plus a small inline script that instructs the browser where to insert it.
This means:
This is directly related to the Core Web Vitals metrics covered in Core Web Vitals in 2026 — Suspense and streaming are practical levers for improving LCP and perceived latency.
Suspense isn't only for SSR. On the client, it works with:
React.lazy() for code splitting (dynamic imports)suspense: true option or React Query)use() hook for reading promises in rendertsximport { lazy, Suspense } from 'react'; const HeavyChart = lazy(() => import('./HeavyChart')); function Dashboard() { return ( <Suspense fallback={<div>Loading chart...</div>}> <HeavyChart /> </Suspense> ); }
This is the same code-splitting pattern described in Code Splitting and Lazy Loading in Next.js — Suspense is the mechanism that makes React.lazy work.
When a suspended component throws an error (not just suspends), the error propagates up to the nearest error boundary, not the Suspense boundary. You typically want to pair them:
tsximport { ErrorBoundary } from 'react-error-boundary'; <ErrorBoundary fallback={<ErrorMessage />}> <Suspense fallback={<Spinner />}> <DataDrivenComponent /> </Suspense> </ErrorBoundary>
For more on designing error boundaries properly, see Error Boundaries and Graceful Failure in React.
Promise.all or by starting fetches before awaiting them.tsxasync function Page() { // Parallel — both fetches start immediately const postsPromise = fetchPosts(); const userPromise = fetchUser(); const [posts, user] = await Promise.all([postsPromise, userPromise]); // ... }
Suspense and streaming work together to break the all-or-nothing constraint of classic SSR. By placing Suspense boundaries at natural loading seams in your component tree, you give React permission to stream content incrementally and show users something meaningful as fast as possible. For a broader view of rendering strategies, the comparison in SSR vs SSG in Next.js provides complementary context on when streaming SSR is the right choice.