Streaming & Suspense
Not waiting until everything is ready
Instead of sending the page once the last fetch is done, Next.js sends it in pieces. Whatever is ready appears immediately.
A server component that takes three seconds holds up the whole page. StreamingThe page is sent in pieces: whatever is ready appears immediately, slow parts follow.→ solves that: the fast part goes out immediately, the slow part follows – and meanwhile the user sees a placeholder.
A restaurant does not hold back every plate because one dish needs another 20 minutes. The starter arrives now, the rest follows.
The easy route: loading.tsx
export default function Loading() {
return <p>Loading …</p>;
} The loading.tsxWhat Next.js shows while the page below is still loading. Behind the scenes it is a Suspense boundary.→ file is nothing but a <Suspense> around the page that Next.js sets up for you. The layout stays put – only the content area shows the placeholder.
If the placeholder roughly has the shape of the eventual content – grey bars where lines will be – the switch feels calmer and the page does not jump. That is cheaper to build than it looks.
The precise route: Suspense per area
loading.tsx covers the whole page. Often only one part is slow. Then you place SuspenseAn area showing a placeholder while something inside it is not ready – the basis for streaming.→ deliberately.
export default async function Page() {
const post = await loadPost(); // 50 ms
const recommendations = await loadRecs(); // 2000 ms
return (
<>
<Post data={post} />
<Recommendations data={recommendations} />
</>
);
} export default async function Page() {
const post = await loadPost(); // 50 ms
return (
<>
<Post data={post} />
<Suspense fallback={<RecsSkeleton />}>
<Recommendations /> {/* fetches itself, blocks nothing */}
</Suspense>
</>
);
} The key move is the relocation: Recommendations now fetches its data itself instead of receiving it as a prop. Only then can the outer component finish without waiting.
export async function Recommendations() {
const data = await loadRecs();
return <ul>{data.map((r) => <li key={r.id}>{r.title}</li>)}</ul>;
} Which one when
- +
loading.tsxwhen the whole page depends on one fetch - +
<Suspense>when only parts are slow – recommendations, comments, statistics - +
<Suspense>also to keep a dynamic part out of an otherwise static page
- −
Putting everything in one Suspense area – then you are back to one spinner for everything
- −
Suspense around things that arrive in 20 ms – that only flickers
- −
Fetching at the top and passing it in: then the outer component waits after all
<Suspense><List data={await fetch()} /></Suspense> achieves nothing. The await sits outside, so the parent waits – and the Suspense boundary never gets the chance to show its placeholder. The fetching has to move into the wrapped component.
▸ In more depth: what happens technically optional
The server keeps the connection open and sends the response in chunks. First comes the HTML with the placeholders, later the follow-up pieces plus an instruction on where they belong.
t=0 ms shell + placeholders go out
→ user already sees header, post, skeleton
t=2000 ms recommendations are ready
→ follow-up goes out, placeholder is replaced
→ no jump, because the skeleton had the same height The follow-up content is part of the same response, not a later JavaScript fetch. So it counts for indexing – unlike data loaded on the client.
Partial PrerenderingServe the static shell immediately and stream the dynamic holes into it – both on one page.→ takes the idea further: the static shell is produced at build time and served from cache, while the dynamic holes inside it stream in per request. That removes the “static or dynamic” decision for a page entirely.
Does it stick?
4 questions on this lesson. Wrong answers show up in your stats.