Skip to content
Home
React Server Components: Streaming, Suspense, and SSR

React Server Components: Streaming, Suspense, and SSR

React React 8 min read 1538 words Beginner ExcellentWiki Editorial Team

React Server Components (RSC) represent the most significant architectural shift in React since hooks. Introduced in React 18 and adopted as the foundation of Next.js 13’s App Router, RSC allows components to run exclusively on the server, reducing the JavaScript sent to the client to nearly zero for those components. The React RFC #188, authored by Dan Abramov, Lauren Tan, and others, describes RSC as “zero-bundle-size components” that can directly access backend resources.

How Server Components Work

Server Components render on the server and produce a serializable description of their output. This output — called the RSC payload — is sent to the client as a stream. The client reconciles this payload with the existing React tree without needing the component’s JavaScript code.

// This component never ships to the client
export default async function Note({ id }) {
  const note = await db.notes.findUnique({ where: { id } });
  return <article><h1>{note.title}</h1><p>{note.body}</p></article>;
---

The key insight is that Server Components can be async because they run on the server. They can directly import database clients, read files, and access environment variables — none of which would be safe on the client.

Client Components vs Server Components

Components in the "use client" boundary are Client Components — they run on the client, have access to hooks, and handle interactivity. Components without the directive are Server Components by default in frameworks like Next.js.

The React documentation provides clear rules:

  • Server Components can import and render Client Components
  • Client Components cannot import Server Components directly (pass them via children)
  • Server Components cannot use hooks, event handlers, or browser APIs
  • Client Components can use all React features

This separation creates a natural boundary: server for data fetching and static rendering, client for interactivity and state.

Streaming and Suspense

Server Components integrate deeply with React Suspense to enable streaming. Instead of waiting for all data to load before sending anything, the server sends HTML progressively as each Suspense boundary resolves:

<Suspense fallback={<Spinner />}>
  <ServerComponentThatFetchesData />
</Suspense>

The server streams the fallback immediately, then replaces it when the async component finishes. The React docs on Suspense in Concurrent Mode explain that this improves perceived performance because the user sees content earlier.

Streaming SSR

Before RSC, server-side rendering (SSR) required the server to fetch all data, render the full HTML, and send it as a single response. With streaming SSR, the initial HTML is sent immediately, and the remaining content arrives in chunks. This eliminates the “all or nothing” problem where a slow data fetch delays the entire page.

The React 18 upgrade guide describes how renderToPipeableStream replaces renderToString for streaming SSR. Next.js and other frameworks use this under the hood.

Server Actions

Server Actions, stabilized in React 19, allow Client Components to invoke server-side functions directly:

"use server";

export async function updateNote(formData) {
  const title = formData.get('title');
  await db.notes.update({ title });
  revalidatePath('/notes');
---

Server Actions are called from forms or event handlers without building API routes. They include built-in CSRF protection, support progressive enhancement (forms work without JavaScript), and automatically revalidate cached data.

The React RFC #229 discusses the design of Server Actions, emphasizing their integration with forms and the progressive enhancement pattern. In frameworks like Next.js, Server Actions are a first-class feature of the App Router.

Suspense and Error Boundaries

RSC integrates with Suspense boundaries for loading states and Error Boundaries for error states. The combination enables co-located loading and error UI for each section of a page:

<ErrorBoundary fallback={<ErrorUI />}>
  <Suspense fallback={<Skeleton />}>
    <ServerComponent />
  </Suspense>
</ErrorBoundary>

This pattern, recommended in the Next.js documentation, gives each component its own loading and error states without affecting the rest of the page.

Data Fetching Patterns

Server Components make data fetching simpler because there is no need for useEffect, loading states, or client-side caching libraries. Direct database access and async component functions replace the fetch-in-effect pattern:

export default async function Page() {
  const posts = await db.posts.findMany({ orderBy: { date: 'desc' } });
  return (
    <ul>
      {posts.map(post => <li key={post.id}>{post.title}</li>)}
    </ul>
  );
---

For cases where you do need client-side data fetching (real-time data, user-specific content), Client Components with libraries like React Query or SWR remain the right choice.

Composing Client and Server Components

The boundary between Client and Server Components requires careful architectural thinking. Server Components cannot use hooks, event handlers, or client-side state. The idiomatic pattern is to push interactive parts to leaf components:

// This is a Server Component (default) — handles data fetching
export default async function ProductPage({ id }) {
  const product = await db.products.findUnique({ where: { id } });
  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      {/* Client Component leaf — handles interactivity */}
      <AddToCart productId={product.id} />
    </div>
  );
---

This “server-heavy, client-leaf” pattern minimizes the client JavaScript bundle while keeping interactivity where it is needed. React’s documentation on Server Components recommends this approach for new projects.

Hybrid Rendering Strategies

Server Components do not replace all other rendering strategies — they complement them. The optimal approach often combines multiple rendering modes:

  • Static rendering: For content that rarely changes (blog posts, documentation), generate at build time
  • Dynamic rendering: For personalized content (dashboards, user profiles), render per request
  • Incremental Static Regeneration (ISR): For content that updates periodically, revalidate cached pages on demand

Next.js and other frameworks let you choose per route or even per component. A product page could statically render the product description while dynamically rendering the inventory count and user-specific pricing.

Static:      /about, /blog/[slug] (revalidate every hour)
Dynamic:     /dashboard, /account
ISR:         /products/[id] (revalidate on demand)

This hybrid approach, documented in the Next.js rendering guide, optimizes for both performance and freshness.

Streaming and Progressive Rendering

RSC’s streaming model enables progressive rendering — showing high-priority content first while lower-priority sections stream in. This is particularly valuable for pages with both fast and slow data dependencies:

// Header renders immediately
<Header />

// Content streams after database query completes
<Suspense fallback={<ContentSkeleton />}>
  <ArticleContent id={params.id} />
</Suspense>

// Comments stream last (slowest query)
<Suspense fallback={<CommentsSkeleton />}>
  <CommentsSection articleId={params.id} />
</Suspense>

The browser displays the header and skeleton loaders immediately, then replaces each skeleton as its data arrives. The user sees meaningful content progressively rather than waiting for a full page load. The streaming architecture also enables early flushing of CSS and fonts, improving perceived performance.

Context in Server Components

React Context is not available in Server Components because context is a client-side feature. Solutions include:

  • Pass serializable data as props through the component tree
  • Use cookies or database lookups for server-side “context” (auth, locale)
  • Wrap only the client portion of the tree with providers in a Client Component

This constraint encourages explicit data flow and reduces hidden dependencies, which improves code clarity and maintainability.

Caching and Revalidation

Server Components integrate with framework-level caching. In Next.js, fetch requests are cached by default with force-cache. The revalidateTag and revalidatePath functions allow targeted cache invalidation:

import { revalidateTag } from 'next/cache';

export async function updatePost(id, data) {
  await db.posts.update({ where: { id }, data });
  revalidateTag(`post-${id}`);
---

The React team’s experimental cache function (separate from Next.js caching) memoizes server function results within a single request, preventing redundant database queries when the same data is requested by multiple components in the same render pass.

Performance Implications

RSC reduces bundle size significantly. A typical page might send 50-80% less JavaScript because server-rendered components do not ship their code to the client. The React team’s demo at React Conf 2021 showed a Notes application where RSC reduced the JavaScript bundle from 240 KB to 30 KB.

However, RSC does not eliminate the need for performance optimization. Server rendering costs CPU time on the server, and large Server Component trees can increase time to first byte (TTFB). Caching, incremental static regeneration, and selective streaming help mitigate these costs.

FAQ

What is the difference between SSR and RSC?

SSR generates HTML on the server and sends it to the client. RSC produces a serializable tree description (RSC payload) that the client merges with its component tree. RSC enables streaming, targeted updates without full page reloads, and zero-bundle-size components.

Do I need to learn a specific framework to use RSC?

RSC is a React feature, but it requires a framework to use. Next.js (App Router) is the most mature RSC implementation. Remix and other frameworks are adding RSC support. The standalone react-server-dom-webpack package exists for custom implementations.

Can I use hooks in Server Components?

No. Server Components cannot use hooks, event handlers, or browser APIs. The "use client" directive marks the boundary where hooks become available.

How do I handle authentication with RSC?

Use Server Components for initial auth checks (reading session cookies, querying the database) and Client Components for the interactive login form. Server Actions handle credential validation and session creation.

Conclusion

React Server Components fundamentally change the React mental model by returning rendering to the server. They reduce bundle sizes, simplify data fetching, and enable streaming without extra client logic. The React RFCs #188 and #229 provide the design rationale, and the Next.js App Router documentation offers practical implementation guidance. To see how RSC integrates with frameworks, read Next.js Guide, and for optimizing performance in mixed Server/Client Component architectures, see React Performance.

For a comprehensive overview, read our article on Next Js Guide.

For a comprehensive overview, read our article on React Authentication.

Section: React 1538 words 8 min read Beginner 756 articles in section Report inaccuracy Back to top