Hirefullstack – Software Engineering & IT-Beratung aus Berlin
← Back to overview
Core 11 min read

Streaming & Suspense

Not waiting until everything is ready

Builds on: Fetching data
In one sentence

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.

Put another way

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

app/blog/loading.tsx
export default function Loading() {
  return <p>Loading …</p>;
}
That is all it takes. Next.js shows it while page.tsx in the same folder is still working.

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.

Tip A skeleton beats a spinner

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.

everything waits for the recommendations
export default async function Page() {
  const post = await loadPost();                 // 50 ms
  const recommendations = await loadRecs();      // 2000 ms

  return (
    <>
      <Post data={post} />
      <Recommendations data={recommendations} />
    </>
  );
}
only the slow part waits
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.

app/Recommendations.tsx
export async function Recommendations() {
  const data = await loadRecs();
  return <ul>{data.map((r) => <li key={r.id}>{r.title}</li>)}</ul>;
}

Which one when

Reach for it when …
  • +

    loading.tsx when 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

Skip it when …
  • 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

Pitfall The most common misconception

<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.

sequence.txt
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
Good to know Search engines get everything too

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.

Tip Looking ahead: partial prerendering

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.

Hirefullstack

Need React firepower on your team?

We have been building React and Next.js applications for clients across Germany for years – as a single expert, as reinforcement for an existing team, or as a complete Scrum team.

Talk about your project →