Skip to content

Suspense SSR — Using React Suspense with Server-Side Rendering

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about Suspense SSR. We cover key concepts, practical examples, and best practices to help you master this topic.

Suspense SSR enables streaming server rendering by wrapping data-fetching components in Suspense boundaries, allowing progressive HTML delivery and faster page loads.

What You'll Learn

By the end of this tutorial, you will understand how React Suspense works with SSR, how to wrap components in Suspense boundaries for streaming, how Suspense integrates with frameworks like Next.js, and best practices for using Suspense in SSR applications.

Why It Matters

Suspense is the key that unlocks streaming SSR. Without Suspense, the server must render everything before sending anything. With Suspense, the server streams the HTML shell immediately, and each Suspense boundary streams its content independently as data becomes available.

Real-World Use

A news homepage uses Suspense boundaries for each section: breaking news (fast API), featured articles (database query), weather widget (external API), and sports scores (slow feed). The page shell appears instantly. Breaking news streams in 200ms. Weather shows in 800ms. Sports scores arrive after 3 seconds. Users read content while other sections still load.

Suspense SSR Streaming Order
    ┌──────────────────────────────────────────────────────────┐
    │         Suspense SSR — Streaming Order                   │
    ├──────────────────────────────────────────────────────────┤
    │                                                          │
    │  Time 0ms:    HTML Shell                                 │
    │               ┌──────────────────────────────────────┐  │
    │               │ Header, Navigation, CSS              │  │
    │               │ Fallback: Loading...                  │  │
    │               │ Fallback: Loading...                  │  │
    │               │ Footer                               │  │
    │               └──────────────────────────────────────┘  │
    │                                                          │
    │  Time 200ms:  Suspense Boundary A (fast API)            │
    │               ┌──────────────────────────────────────┐  │
    │               │ Breaking News Section                │  │
    │               │ (replaces fallback)                  │  │
    │               └──────────────────────────────────────┘  │
    │                                                          │
    │  Time 800ms:  Suspense Boundary B (database)            │
    │               ┌──────────────────────────────────────┐  │
    │               │ Featured Articles                    │  │
    │               │ (replaces fallback)                  │  │
    │               └──────────────────────────────────────┘  │
    │                                                          │
    │  Time 3s:     Suspense Boundary C (slow API)            │
    │               ┌──────────────────────────────────────┐  │
    │               │ Sports Scores                        │  │
    │               │ (replaces fallback)                  │  │
    │               └──────────────────────────────────────┘  │
    │                                                          │
    └──────────────────────────────────────────────────────────┘

Think of Suspense SSR like a restaurant serving a multi-course meal. Instead of waiting for all courses to be ready before serving anything, the waiter brings each course as it is ready. The appetizer (shell) comes immediately. The main course (fast content) follows shortly. The dessert (slow content) arrives last. You start eating while the kitchen finishes the remaining courses.

Suspense with Data Fetching

import { Suspense } from 'react';

// Data-fetching component (uses a framework like SWR or React Query)
async function BreakingNews() {
    const news = await fetchNews(); // Suspends while fetching
    return (
        <div className="breaking-news">
            <h2>Breaking News</h2>
            {news.map(item => (
                <article key={item.id}>
                    <h3>{item.title}</h3>
                    <p>{item.summary}</p>
                </article>
            ))}
        </div>
    );
}

// Simplified fallback
function NewsSkeleton() {
    return (
        <div className="skeleton">
            <div className="skeleton-title" />
            <div className="skeleton-text" />
            <div className="skeleton-text" />
        </div>
    );
}

// Parent component with Suspense boundaries
function HomePage() {
    return (
        <div>
            <header>
                <h1>News Portal</h1>
                <nav>...</nav>
            </header>

            <main>
                {/* Each Suspense boundary streams independently */}
                <Suspense fallback={<NewsSkeleton />}>
                    <BreakingNews />
                </Suspense>

                <Suspense fallback={<ArticleSkeleton />}>
                    <FeaturedArticles />
                </Suspense>

                <Suspense fallback={<WeatherSkeleton />}>
                    <WeatherWidget />
                </Suspense>
            </main>

            <footer>...</footer>
        </div>
    );
}

Suspense in Next.js App Router

// app/page.js — Next.js App Router with Suspense
import { Suspense } from 'react';

// Server Components with async data fetching
async function ProductGrid() {
    const products = await fetch('https://api.example.com/products', {
        cache: 'no-store'
    }).then(r => r.json());

    // This component suspends while data fetches
    // On the server, it streams when ready
    return (
        <div className="grid">
            {products.map(product => (
                <ProductCard key={product.id} product={product} />
            ))}
        </div>
    );
}

function ProductGridSkeleton() {
    return (
        <div className="grid">
            {Array.from({ length: 6 }).map((_, i) => (
                <div key={i} className="skeleton-card">
                    <div className="skeleton-image" />
                    <div className="skeleton-title" />
                    <div className="skeleton-price" />
                </div>
            ))}
        </div>
    );
}

export default function HomePage() {
    return (
        <div>
            <h1>Product Catalog</h1>
            <Suspense fallback={<ProductGridSkeleton />}>
                <ProductGrid />
            </Suspense>
        </div>
    );
}

// loading.js — Next.js automatically wraps page in Suspense
// app/products/loading.js
export default function Loading() {
    return (
        <div className="page-skeleton">
            <div className="skeleton-header" />
            <div className="skeleton-content" />
        </div>
    );
}

Common Mistakes

  1. Nesting Suspense boundaries incorrectly. Suspense boundaries should wrap individual data-fetching components, not large sections. Overly large boundaries delay showing any content.
  2. Not providing meaningful fallbacks. A blank div or text spinner is poor UX. Use skeleton screens that match the content layout for a smooth experience.
  3. Too many Suspense boundaries. Each boundary adds overhead. Group related data fetches in the same boundary. A page should have 3-5 Suspense boundaries at most.
  4. Using Suspense without a data framework. React Suspense needs a data fetching framework (SWR, React Query, Relay) that integrates with Suspense. Plain fetch does not suspend automatically.
  5. Forgetting about the loading state. Suspense only handles the initial loading. For subsequent data refetches, use useTransition or separate loading states.

Practice Questions

  1. How does Suspense enable streaming SSR?
  2. What is a Suspense boundary and how do you create one?
  3. Why should you use skeleton screens as fallbacks?
  4. How does the Next.js App Router integrate with Suspense?
  5. What is the difference between Suspense boundaries and loading.js?

Challenge: Build a news homepage with 3 Suspense boundaries: breaking news (1s delay), featured articles (2s delay), and weather widget (3s delay). Each boundary should have an appropriate skeleton fallback. Measure the time each section appears. Compare with a non-Suspense version.

FAQ

Does Suspense work with all data fetching?

Suspense works with data fetching libraries that implement the Suspense contract (throw a promise). React Query, SWR, Relay, and Next.js Server Components support Suspense.

Can I use Suspense with React 17?

No. Suspense for data fetching requires React 18. React 17 only supports Suspense for lazy-loaded components (React.lazy).

How many Suspense boundaries should a page have?

3-5 is a good range. Too few means large sections load at once. Too many adds overhead. Group related content that loads at similar times.

Does Suspense affect SEO?

Meta tags in Suspense boundaries may not be seen by crawlers if they are streamed late. Ensure critical SEO content (title, meta description) is in the initial shell, not in Suspense boundaries.

How do I handle errors in Suspense?

Use Error Boundaries alongside Suspense. Wrap each Suspense boundary in an ErrorBoundary to catch errors in specific sections without crashing the entire page.

Mini Project

Build a dashboard page with 4 Suspense boundaries: user stats (fast cache), activity feed (API), notifications (slow), and recommendations (complex query). Each boundary has a skeleton fallback. Implement error boundaries for each section. Measure and compare streaming vs non-streaming page load.

What's Next

You understand Suspense SSR. Now explore Progressive Hydration to hydrate components incrementally for better interactivity.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro