Picovert

React Server Components: Complete Guide for 2026

2026-04-279 min read

React Server Components (RSC) ship zero bytes of their own JavaScript to the browser. They run exclusively on the server, stream HTML to the client, and can fetch data directly without an API layer. In Next.js App Router, every component is a Server Component by default. This guide explains the mental model, the boundaries between server and client, and the patterns that work well at scale.

The core idea

Before RSC, every React component shipped its JavaScript to the browser. A component that fetches a database row, formats a date, and renders a card would include the database driver, the date library, and the rendering logic — all in the browser bundle, even though the browser never calls the database.

RSC changes this: server components run only on the server. Their output is serialized into a special format (RSC payload) and streamed to the client. The client receives rendered HTML and a small reconciliation payload — not the component's source code.

Server vs Client Components

FeatureServer ComponentClient Component
Runs on serverYesYes (hydration)
Runs on clientNoYes
Ships JS to browserNoYes
Direct DB/filesystem accessYesNo
useState, useEffectNoYes
Event handlers (onClick etc.)NoYes
Browser APIs (window, document)NoYes

Mark a component as a Client Component by adding "use client" at the top of the file. This is the opt-in boundary. Every component that imports a Client Component also becomes part of the client bundle — the boundary propagates downward.

Data fetching in Server Components

Server Components can be async functions. You can await data directly:

async function ProductPage({ id }) { const product = await db.products.findById(id); return <ProductCard product={product} />; }

No useEffect, no loading state, no API route. The component suspends while fetching, and React streams the result to the client when ready. Wrap in <Suspense> to show a fallback while waiting.

The Suspense streaming pattern

Suspense boundaries allow Next.js to stream HTML in chunks. Fast components render and ship immediately; slow ones render later, streamed into the already-painted shell:

  • Page shell — nav, layout, headers — renders instantly.
  • Hero content — critical data, fast DB query — renders in ~50 ms.
  • Recommendations, reviews — slower queries — render when ready, replacing skeleton placeholders.

The user sees a progressively complete page instead of a blank screen followed by a sudden full paint.

Common patterns

Pass server data to client components as props

Server Components can import Client Components and pass them data. The data is serialized (must be JSON-serializable — no class instances, no functions) and passed as props:

// ServerComponent.tsx (no 'use client') import { InteractiveWidget } from './InteractiveWidget'; // 'use client' export async function Page() { const data = await fetch(...); return <InteractiveWidget data={data} />; }

Keep event handlers in Client Components

Any component that uses onClick, onChange, useState, or browser APIs must be marked "use client". Keep these components small and push them toward the leaves of the component tree to minimize bundle size.

Don't import heavy libraries in Server Components

The advantage of RSC is that library code stays on the server. But if a Server Component imports a library that is only available in browser environments, the build will fail. Use dynamic imports with { ssr: false } for browser-only code.

When NOT to use Server Components

  • Real-time interactions — chat, live dashboards, collaborative editing. These need client state and WebSocket connections.
  • Highly interactive UIs — drag-and-drop, complex form state, canvas-based editors.
  • Components that read browser APIs — geolocation, clipboard, local storage.

Performance impact

Real-world Next.js apps that adopt RSC see 30–60% reductions in JavaScript bundle size for pages that previously fetched data client-side. LCP improves because the initial HTML contains real content instead of loading skeletons. INP improves because there's less JavaScript for the browser to parse and execute on load.