Next.js cookbook · Intermediate

Stream slow sections with Suspense

Render the page shell instantly while slow data (recommendations, analytics) loads.

Approach

Fetch fast, essential data in the page and wrap slow async Server Components in <Suspense> with skeleton fallbacks so Next.js streams HTML progressively. Use loading.tsx for a route-level fallback. Start independent fetches in parallel to avoid waterfalls.

Solution

// app/products/[id]/page.tsx
import { Suspense } from "react";

export default async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const product = await getProduct(id);                      // fast, needed for the shell
  const reviewsPromise = getReviews(id);                     // start early, don't await yet

  return (
    <main>
      <ProductDetails product={product} />
      <Suspense fallback={<ReviewsSkeleton />}>
        <Reviews promise={reviewsPromise} />
      </Suspense>
      <Suspense fallback={<RecommendationsSkeleton />}>
        <Recommendations productId={id} />                    {/* slow service */}
      </Suspense>
    </main>
  );
}

async function Reviews({ promise }: { promise: Promise<Review[]> }) {
  const reviews = await promise;
  return <ReviewList reviews={reviews} />;
}

async function Recommendations({ productId }: { productId: string }) {
  const items = await getRecommendations(productId);         // may take 1–2 s
  return <ProductCarousel items={items} />;
}

Pitfalls

  • Awaiting all data at the top of the page (no streaming).
  • Sequential awaits that could run in parallel.
  • Fallbacks that cause large layout shifts (size skeletons correctly).

Variations

  • loading.tsx per route segment for navigation-level loading UI.
  • Partial Prerendering / Cache Components to serve a static shell with dynamic holes.

Further reading: Next.js: Streaming

Reference: https://nextjs.org/docs/app/getting-started/fetching-data#streaming

More Next.js recipes