Server Components

React Server Components (RSC) let you write components that run only on the server. They send rendered output to the client, never their JavaScript. This is the default in React 19 frameworks - you opt in to client code, not out.

What RSC Actually Is

Server components are regular React components with two constraints: no hooks, no browser APIs. In exchange, they get direct access to server resources (databases, file system, private APIs) and send zero JavaScript to the client.

// This component runs on the server only
// No "use client" directive = server component by default
import { db } from "@/lib/db";

async function RecentPosts() {
  const posts = await db.posts.findMany({
    orderBy: { createdAt: "desc" },
    take: 10,
  });

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>
          <a href={`/posts/${post.slug}`}>{post.title}</a>
          <time>{post.createdAt.toLocaleDateString()}</time>
        </li>
      ))}
    </ul>
  );
}

This component’s code never reaches the browser. The database driver, the query, the ORM - none of it is in the client bundle.

RSC Payload (Not Just HTML)

Server components don’t send plain HTML to the client. They send an RSC payload - a serialized React tree that the client runtime can reconcile with the existing UI.

This matters because:

  1. Client state is preserved during navigation. A <SearchInput> keeps its typed text even when the page around it updates from the server.
  2. Streaming works out of the box. Suspense boundaries in server components stream their resolved content as it becomes available.
  3. Interleaving server and client components is seamless. The payload describes where client components slot in.
// Simplified RSC payload (what actually goes over the wire)
0:["$","div",null,{"children":[
  ["$","h1",null,{"children":"Dashboard"}],
  ["$","$Lclient-counter",null,{"initialCount":5}],  // client component reference
  ["$","ul",null,{"children":[                        // server-rendered list
    ["$","li","post-1",{"children":"First Post"}],
    ["$","li","post-2",{"children":"Second Post"}]
  ]}]
]}]

The “use client” Boundary

"use client" marks the boundary where server transitions to client. Everything below that import is client-side.

// Dashboard.tsx - server component (no directive)
import { db } from "@/lib/db";
import { StatsChart } from "./StatsChart"; // client component

async function Dashboard() {
  const stats = await db.stats.getWeekly();

  return (
    <div>
      <h1>Dashboard</h1>
      {/* Server fetches data, passes it as props to client component */}
      <StatsChart data={stats} />
      <RecentActivity /> {/* another server component */}
    </div>
  );
}
// StatsChart.tsx - needs interactivity
"use client";

import { useState } from "react";

export function StatsChart({ data }: { data: WeeklyStat[] }) {
  const [range, setRange] = useState<"week" | "month">("week");

  return (
    <div>
      <select value={range} onChange={(e) => setRange(e.target.value as any)}>
        <option value="week">Week</option>
        <option value="month">Month</option>
      </select>
      <Chart data={data} range={range} />
    </div>
  );
}

The Boundary Rules

Server Component (default)
├── Can import other server components ✓
├── Can import client components ✓
├── Can use async/await ✓
├── Can access server resources ✓
└── Cannot use hooks or browser APIs ✗

Client Component ("use client")
├── Can import other client components ✓
├── Cannot import server components directly ✗
├── Can receive server components as children ✓
├── Can use hooks and browser APIs ✓
└── Cannot access server resources ✗

Gotcha: You can’t import a server component inside a "use client" file. But you can pass server components as children or other props to client components. This is the “donut pattern”:

// Layout.tsx - server component
import { Sidebar } from "./Sidebar"; // client component
import { NavLinks } from "./NavLinks"; // server component

function Layout({ children }: { children: React.ReactNode }) {
  return (
    <Sidebar>
      {/* NavLinks is a server component passed as children to a client component */}
      <NavLinks />
    </Sidebar>
  );
}
// Sidebar.tsx
"use client";

import { useState } from "react";

export function Sidebar({ children }: { children: React.ReactNode }) {
  const [open, setOpen] = useState(true);
  return (
    <aside className={open ? "open" : "closed"}>
      <button onClick={() => setOpen(!open)}>Toggle</button>
      {children} {/* server-rendered NavLinks appears here */}
    </aside>
  );
}

Bundle Size Impact

Server components have a direct impact on what ships to the browser.

Component typeJS sent to clientExample savings
Server component using marked (markdown parser, 60KB)0 KB60 KB saved
Server component using date-fns (25KB)0 KB25 KB saved
Client component using date-fns25 KB(no savings)
Server component with direct DB access0 KBEntire ORM saved

A typical app migrating to RSC sees 30-50% reduction in client JavaScript. The savings are automatic - you don’t need to configure code splitting or lazy loading for server components.

Framework Support

RSC requires a framework with a compatible bundler. You can’t just use RSC with plain Vite (yet).

FrameworkRSC SupportStatus
Next.js (App Router)FullProduction-ready since Next 13.4
RemixExperimentalBehind a flag, API still changing
WakuMinimalRSC-first, lightweight alternative
Plain ViteNoneNo bundler integration for RSC

Tip: If you’re starting a new project and want RSC, Next.js App Router is the only production-ready option. If you don’t need RSC, Vite + React works great for client-side apps.

When to Use Server vs Client

Decision guide:

  • Need to fetch data? Server component (or server action). Avoid useEffect fetch-on-mount when possible.
  • Need onClick, onChange, or any event handler? Client component.
  • Need useState, useEffect, or other hooks? Client component.
  • Rendering static or data-driven content with no interactivity? Server component.
  • Using a heavy library for rendering (syntax highlighting, markdown)? Server component to keep it off the bundle.

Start with server components. Add "use client" only when you need interactivity. Push the boundary as far down the tree as possible - a page can be 90% server components with small interactive client leaves.

Next: Setup to start building with React