Technology · Next.js
Next.js Streaming and Progressive Rendering
Stream content progressively with Suspense to improve perceived performance and UX.
TL;DR
- 01Use Suspense to stream content as it's ready.
- 02Show loading states while slow components render.
- 03Stream improves perceived performance and Core Web Vitals.
Suspense Boundaries
- Use Suspense to show loading states while data loads.
import { Suspense } from "react"; import SlowComponent from "./SlowComponent"; export default function Page() { return ( <Suspense fallback={<p>Loading...</p>}> <SlowComponent /> </Suspense> ); } - Content is streamed as soon as it's ready.
- Users see the page progressively instead of waiting.
- Use a
loading.tsxfile as a route-level Suspense boundary.// app/dashboard/loading.tsx export default function Loading() { return <div className="skeleton">Loading dashboard...</div>; } - Suspense boundaries catch async server components automatically in App Router.
// Any async server component inside Suspense is streamed async function SlowData() { const data = await fetchSlowAPI(); // streamed when ready return <ul>{data.map(d => <li key={d.id}>{d.name}</li>)}</ul>; }
Nested Suspense Boundaries
- Create multiple boundaries for granular control.
export default function Dashboard() { return ( <div> <Header /> <Suspense fallback={<p>Loading posts...</p>}> <Posts /> </Suspense> <Suspense fallback={<p>Loading sidebar...</p>}> <Sidebar /> </Suspense> </div> ); } - Each boundary loads independently.
- Fast components render immediately.
- Slow components show loading states separately.
- Nest boundaries to control exactly which sections block each other.
<Suspense fallback={<HeaderSkeleton />}> <Header /> <Suspense fallback={<FeedSkeleton />}> <Feed /> </Suspense> </Suspense> - Outer boundary shows while inner boundaries resolve independently.
Server Components with Streaming
- Server components naturally stream their data.
// app/page.tsx import { Suspense } from "react"; import Header from "./components/Header"; import Post from "./components/Post"; export default function Home() { return ( <> <Header /> <Suspense fallback={<p>Loading posts...</p>}> <Post /> </Suspense> </> ); } // components/Post.tsx (server component) async function Post() { const posts = await fetch("https://api/posts").then(r => r.json()); return posts.map(post => <article key={post.id}>{post.title}</article>); } - Server components fetch data directly.
- Data streams as soon as it's ready.
- Parallel data fetching inside server components speeds up streaming.
async function Page() { // Both fetches start at the same time const [user, posts] = await Promise.all([ fetchUser(), fetchPosts() ]); return <Profile user={user} posts={posts} />; } - Avoid waterfalls by fetching all needed data in parallel.
Progressive Enhancement
- Load critical content first, enhance with additional data.
export default function Product({ id }) { return ( <div> <Suspense fallback={<p>Loading product...</p>}> <BasicProduct id={id} /> </Suspense> <Suspense fallback={<p>Loading reviews...</p>}> <Reviews id={id} /> </Suspense> <Suspense fallback={<p>Loading recommendations...</p>}> <Recommendations id={id} /> </Suspense> </div> ); } - Show essential content immediately.
- Load secondary content in the background.
- Prioritize above-the-fold content with the first Suspense boundary.
<main> <HeroSection /> {/* renders synchronously, no Suspense */} <Suspense fallback={<Skeleton />}> <BelowFoldContent /> {/* streams in after hero */} </Suspense> </main> - Deferred content improves Largest Contentful Paint score.
State Transition Updates
- Keep UI responsive during state changes.
import { useTransition } from "react"; export default function Page({ id }) { const router = useRouter(); const [isPending, startTransition] = useTransition(); function handleNavigate() { startTransition(() => { router.push(`/product/${id}`); }); } return ( <> {isPending && <p>Loading...</p>} <button onClick={handleNavigate}> View Product </button> </> ); } - useTransition keeps UI responsive during async operations.
- Show loading state without blocking interaction.
- Mark expensive state updates as non-urgent with startTransition.
const [isPending, startTransition] = useTransition(); function handleFilter(value) { setInputValue(value); // urgent: update input immediately startTransition(() => { setFilteredList(filterItems(value)); // non-urgent: can wait }); } - useDeferredValue defers a value update to keep UI responsive.
const deferredQuery = useDeferredValue(query); // deferredQuery lags behind query, preventing expensive re-renders return <Results query={deferredQuery} />;
Tip: Use streaming to show partial content quickly — users perceive the page as faster even if all data isn't ready yet.
Warning: Don't create too many Suspense boundaries — keep them at logical boundaries to avoid confusing loading states.