DEV Community

Cover image for React Server Components vs traditional SSR: what changed and why it matters
Amit Shukla
Amit Shukla

Posted on Originally published at amitshuklabag.hashnode.dev

React Server Components vs traditional SSR: what changed and why it matters

People use "SSR" and "Server Components" like they're the same idea wearing a different name. They're not. Traditional server side rendering and React Server Components solve different problems, and mixing them up leads to either overbuilding a simple page or missing out on a real reduction in what you ship to the browser.

What traditional SSR actually does

Server side rendering, the kind React has done since renderToString existed, runs your entire component tree on the server for the first request and sends back real HTML. That's the win: the user sees content immediately instead of a blank page waiting for JavaScript to load and run.

But the server's job isn't done once that HTML ships. The client still downloads the JavaScript for every component in that tree, then re-runs it in the browser to attach event handlers and make the page interactive. That step is hydration, and it means the same render work happens twice, once on the server to produce HTML, once on the client to make it interactive, and the client has to download and execute the code for all of it, including the parts of the page that never do anything interactive at all.

A list of blog post previews with no buttons, no state, nothing to click, still ships its full component code to the browser and still gets hydrated, purely because it was part of the same tree as something that actually needed interactivity.

What Server Components actually change

A Server Component runs only on the server. Not "server first, then also client." Only the server. Its code, and its dependencies, never reach the browser at all. What gets sent to the client is a special serialized description of the rendered output, not the component's source, not its imports, none of it.

Interactivity still exists, you opt into it explicitly with 'use client' at the top of a file. That marks a component as one that does need to run and hydrate in the browser, because it holds state, uses a hook, or responds to events. Everything else, by default, stays server-only.

This is an actual architectural split, not a performance tweak. You're choosing, per component, whether its code ships to the browser at all.

Before and after

A traditional approach: the whole page, including the static list, is one client-rendered (or hydrated) tree.

// everything here ships to the browser and gets hydrated
export default function PostList({ posts }) {
  return (
    <ul>
      {posts.map((p) => (
        <li key={p.id}>
          {p.title}
          <LikeButton postId={p.id} />
        </li>
      ))}
    </ul>
  );
}
Enter fullscreen mode Exit fullscreen mode

With Server Components, the list itself, and the data fetching behind it, stays on the server. Only the interactive part opts in.

// PostList.jsx — Server Component, never ships to the client
export default async function PostList() {
  const posts = await db.posts.findMany(); // direct backend access, no API route
  return (
    <ul>
      {posts.map((p) => (
        <li key={p.id}>
          {p.title}
          <LikeButton postId={p.id} />
        </li>
      ))}
    </ul>
  );
}
Enter fullscreen mode Exit fullscreen mode
// LikeButton.jsx — Client Component, this is the only part hydrated
'use client';
export default function LikeButton({ postId }) {
  const [liked, setLiked] = useState(false);
  return <button onClick={() => setLiked(!liked)}>{liked ? 'Liked' : 'Like'}</button>;
}
Enter fullscreen mode Exit fullscreen mode

Same visual result. The list's rendering code, its data fetching logic, and its dependencies never leave the server. The only JavaScript the browser downloads and hydrates is the tiny like button.

What this actually buys you

Smaller client bundles. Not marginally, meaningfully, because entire categories of code, data access layers, markdown parsers, formatting libraries, anything only used to produce the server-rendered output, simply never ship.

Direct backend access. A Server Component can query a database or call an internal service directly, no separate API route needed just to get data into the page.

Streaming by default. Server Components can stream their output as it becomes ready, so slow data doesn't block fast data from rendering.

Where this gets misused

It needs framework support. This isn't a flag you flip in any React app, it's a rendering convention that Next.js's App Router (and a small number of other frameworks) actually implements. Reaching for it outside that support gets you nothing.

No hooks, no state, no browser APIs inside a Server Component. useState, useEffect, window, none of it works there, because the component never runs in a browser. That logic has to live in a Client Component.

Sequential fetches still create waterfalls. Moving data fetching to the server doesn't automatically parallelize it. Awaiting one fetch before starting the next is still slow, on the server or the client, fix it the same way you always would, start the requests together and await them together.

Takeaway

Traditional SSR answers "how do we get HTML to the browser fast." Server Components answer a different question: "does this component's code need to reach the browser at all." Most pages have far more content that never needed to be there than interactive pieces that do, and that's exactly the part Server Components let you stop shipping.

Top comments (0)