Course outline · 0% complete

0/29 lessons0%

Course overview →

Streaming parts of a page with Suspense

lesson 4-4 · ~9 min · 14/29

When one slow widget holds the page hostage

loading.js has a blunt edge: it replaces the entire page until all of the page's data resolves. Real pages are rarely uniformly slow. A dashboard's header and nav render in milliseconds while one revenue chart waits two seconds on an analytics API. Making users stare at a full-page skeleton because of one widget is the problem streaming solves, and it is why production dashboards feel fast even when some of their data is slow.

The tool is React's Suspense component, which you can place around any slow subtree yourself:

// app/dashboard/page.js
import { Suspense } from "react";
import RevenueChart from "./revenue-chart"; // async server component, slow fetch

export default function DashboardPage() {
  return (
    <>
      <h1>Dashboard</h1>
      <Suspense fallback={<p>Loading revenue…</p>}>
        <RevenueChart />
      </Suspense>
    </>
  );
}

The server sends the heading, and everything else that is fast, immediately, with the fallback sitting in the chart's slot. When RevenueChart's data resolves, the server streams the finished HTML down the same response and React slots it in. There is no extra request and no full reload.

time →0 msheader + nav HTMLfallbackuser reads the page2000 msstill on screenchart HTMLstreamed into the slotOne response, no second request. Content outside theSuspense boundary never waits for the slow fetch.
Streaming with Suspense: the fast header and nav are sent immediately with a fallback in the slow widget's slot, then the finished widget HTML streams into the same response when its data resolves.

loading.js was Suspense all along

Now the Unit 4 picture completes. loading.js is just Next.js wrapping your whole page in one Suspense boundary whose fallback is that file. A hand-placed Suspense is the same mechanism at finer grain.

loading.jsHand-placed Suspense
Scope of the waitThe whole pageOne subtree
Extra codeNone, one fileA wrapper per slow region
Best whenThe page has one main fetchThe page is fast except for a widget or two
Fast content paintsAfter all data resolvesImmediately

The two compose rather than compete: loading.js covers the navigation itself, and inner Suspense boundaries cover the stragglers within the page.

One discipline note matters more than the choice between them. The slow fetch must live inside the wrapped component. If the page component itself awaits the data and passes it down as props, the page has already blocked before rendering begins, so nothing streams and the boundary buys you nothing.

What streams first on a dashboard

Take a dashboard page that renders a fast header, then <Suspense fallback={<Spinner/>}><SlowChart/></Suspense>. During SlowChart's two-second fetch the user sees the header immediately, with the spinner sitting where the chart will appear.

Suspense scopes the waiting to its own subtree, and three things follow from that:

  • Everything outside the boundary streams to the browser immediately.
  • The fallback occupies the chart's slot, so the layout does not jump around.
  • When the data resolves, the server streams the finished chart HTML into place.

A blank page, or a whole-page loading.js skeleton triggered by one slow widget, is exactly what per-widget Suspense avoids.

Isolating a slow recommendations widget

Consider a home page that is fast except for a <Recommendations/> server component fetching from a slow machine-learning service. The component to reach for is Suspense, wrapping just that subtree:

<Suspense fallback={<Skeleton />}>
  <Recommendations />
</Suspense>

The rest of the page streams to the browser immediately, the fallback holds the slot so nothing shifts when the content lands, and the recommendations HTML streams in when the service finally responds.

Why it works out that way

  • Suspense takes a fallback prop and ships with React itself, so there is nothing to install.
  • loading.js is this same component applied automatically to the whole page, which is too coarse when only one widget is slow.
  • The slow fetch has to stay inside Recommendations itself. Hoisting it into the page component would make the page block before streaming ever starts.