# How React SSR Differs from React Server Components (RSC) in Next.js

> Understand the core differences between React SSR and React Server Components in Next.js. Learn how RSCs optimize performance by reducing client bundles and sending only essential code to the browser.

- Repository: [Vercel/next.js](https://github.com/vercel/next.js)
- Tags: deep-dive
- Published: 2026-02-20

---

**React SSR generates full HTML strings on every request that are hydrated by the client, while React Server Components execute exclusively on the server to stream a serialized RSC payload, enabling smaller client bundles and progressive hydration without sending server-only code to the browser.**

Next.js supports two distinct server-rendering architectures that solve different performance and developer experience challenges. While both React SSR and React Server Components (RSC) execute on the server, the **vercel/next.js** codebase implements them through entirely separate pipelines with different output formats, bundle semantics, and data-fetching patterns.

## Architectural Differences: HTML Strings vs. Serialized Payloads

### Execution Model and Output Format

With traditional **React SSR**, the code runs on every request to produce a complete HTML string via `renderToHTML` (implemented in [`packages/next/src/server/base-server.ts`](https://github.com/vercel/next.js/blob/main/packages/next/src/server/base-server.ts)). The server sends this final markup to the browser, and the client hydrates it into an interactive application.

In contrast, **React Server Components** run **only on the server** to generate an `RSCPayload` — a special JSON-like format created by `generateDynamicRSCPayload` in [`packages/next/src/server/app-render/app-render.tsx`](https://github.com/vercel/next.js/blob/main/packages/next/src/server/app-render/app-render.tsx). The client-side React runtime consumes this streamed payload and reconstructs the UI without requiring the component code itself.

### What the Client Receives

SSR responses include pure HTML plus a JavaScript bundle containing all imports, including data-fetching libraries. This means code used exclusively for server-side logic still ends up in the client bundle.

RSC responses send an **HTML shell** instantly, followed by the RSC stream (detected via the `RSC_HEADER` defined in [`packages/next/src/server/app-render/app-render.tsx`](https://github.com/vercel/next.js/blob/main/packages/next/src/server/app-render/app-render.tsx)). The **React Flight** loader in [`packages/next/src/build/webpack/plugins/next-flight-plugin.ts`](https://github.com/vercel/next.js/blob/main/packages/next/src/build/webpack/plugins/next-flight-plugin.ts) strips server-only imports from client bundles, ensuring [`.server.tsx`](https://github.com/vercel/next.js/blob/main/.server.tsx) files never reach the browser.

## Data Fetching and Bundle Size Implications

### Traditional SSR Data Fetching

SSR requires explicit data fetching through `export async function getServerSideProps(context)`. This function runs per request in [`packages/next/src/server/route-modules/pages/module.ts`](https://github.com/vercel/next.js/blob/main/packages/next/src/server/route-modules/pages/module.ts), which merges the returned `props` into the HTML response. Any modules imported for data fetching are included in the client JavaScript.

### Direct Data Access in RSC

Inside [`.server.tsx`](https://github.com/vercel/next.js/blob/main/.server.tsx) files, you can `await` data directly in the component body using server-only modules like `node-fetch`. The data becomes part of the serialized `RSCPayload` without explicit prop drilling. Because the component executes server-side and only the result streams to the client, heavy logic and dependencies remain entirely on the server, drastically reducing bundle size.

## Streaming, Caching, and Error Resilience

SSR through `renderToHTMLImpl` can stream HTML, but the **HTML must be fully generated** before the response is considered complete. Errors thrown in `getServerSideProps` or during rendering abort the entire page generation, typically resulting in a generic error page.

RSC is **inherently streaming** via `getFlightStream`. The HTML shell arrives first, followed by partial RRC slices as they become available. Errors are isolated to specific components; [`app-render.tsx`](https://github.com/vercel/next.js/blob/main/app-render.tsx) embeds error placeholders in the payload while the rest of the page continues rendering, enabling fine-grained error boundaries and progressive hydration.

## Implementation in the Next.js Source Code

### The SSR Pipeline

The core SSR implementation resides in [`packages/next/src/server/route-modules/pages/module.ts`](https://github.com/vercel/next.js/blob/main/packages/next/src/server/route-modules/pages/module.ts), which orchestrates the call to `BaseServer.renderToHTMLImpl` in [`packages/next/src/server/base-server.ts`](https://github.com/vercel/next.js/blob/main/packages/next/src/server/base-server.ts). This pipeline handles `getServerSideProps` parsing, HTML generation, and the merging of server-side data into the final markup.

### The RSC Pipeline

The RSC entry point is [`packages/next/src/server/app-render/app-render.tsx`](https://github.com/vercel/next.js/blob/main/packages/next/src/server/app-render/app-render.tsx), which detects the `RSC` header and builds the payload via `generateDynamicRSCPayload`. The streaming response is managed by [`use-flight-response.tsx`](https://github.com/vercel/next.js/blob/main/use-flight-response.tsx) (referenced internally by the app renderer). Server-only component detection happens at build time through [`packages/next/src/build/webpack/plugins/next-flight-plugin.ts`](https://github.com/vercel/next.js/blob/main/packages/next/src/build/webpack/plugins/next-flight-plugin.ts), which ensures incompatible imports are excluded from client bundles.

## Code Examples

### Traditional SSR with getServerSideProps

```typescript
// pages/profile.tsx
import type { GetServerSideProps } from 'next'

export const getServerSideProps: GetServerSideProps = async (ctx) => {
  const res = await fetch(`https://api.example.com/user/${ctx.params?.id}`)
  const user = await res.json()
  return { props: { user } }
}

export default function Profile({ user }: { user: any }) {
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.bio}</p>
    </div>
  )
}

```

`getServerSideProps` executes in [`packages/next/src/server/route-modules/pages/module.ts`](https://github.com/vercel/next.js/blob/main/packages/next/src/server/route-modules/pages/module.ts) on every request, and the resulting `props` are serialized into the HTML generated by `renderToHTML`.

### React Server Component with Server-Only Imports

```typescript
// app/dashboard/profile.server.tsx
import fetch from 'node-fetch' // ✅ server-only import

export default async function Profile() {
  const res = await fetch('https://api.example.com/user/me')
  const user = await res.json()
  return (
    <section>
      <h1>{user.name}</h1>
      <p>{user.bio}</p>
    </section>
  )
}

```

The component runs **only on the server**. Next.js streams the resulting RSC payload from `generateDynamicRSCPayload` in [`app-render.tsx`](https://github.com/vercel/next.js/blob/main/app-render.tsx), and the client receives a lightweight HTML shell that hydrates when the payload arrives.

### Combining SSR and RSC

```typescript
// app/dashboard/page.tsx
import Profile from './profile.server'   // RSC
import { getServerSideProps } from '../../pages/api/preview' // traditional SSR hook

export const getServerSideProps = async (ctx) => {
  // Example: set a preview cookie for the whole page
  return { props: {} }
}

export default function Dashboard() {
  return (
    <main>
      <h2>Dashboard (SSR page)</h2>
      {/* RSC will stream separately */}
      <Profile />
    </main>
  )
}

```

This demonstrates incremental adoption: the outer page renders via `renderToHTML` (SSR), while the `<Profile />` component streams as an isolated RSC payload.

## Summary

- **React SSR** produces complete HTML strings on each request via `renderToHTML`, suitable for SEO-critical pages but requiring full hydration and sending all imports to the client.
- **React Server Components** stream a serialized `RSCPayload` via `generateDynamicRSCPayload`, enabling progressive hydration and zero client JavaScript for server-only logic.
- SSR bundles all imported modules for the client; RSC strips server-only code through the Flight plugin ([`next-flight-plugin.ts`](https://github.com/vercel/next.js/blob/main/next-flight-plugin.ts)).
- Data fetching in SSR requires `getServerSideProps` in [`pages/module.ts`](https://github.com/vercel/next.js/blob/main/pages/module.ts); RSC allows direct `await` inside async components executing in [`app-render.tsx`](https://github.com/vercel/next.js/blob/main/app-render.tsx).
- RSC provides granular error boundaries and streaming by default, while SSR errors in [`base-server.ts`](https://github.com/vercel/next.js/blob/main/base-server.ts) fail the entire page render.

## Frequently Asked Questions

### Can I use both SSR and RSC in the same Next.js application?

Yes. Next.js supports incremental adoption, allowing you to mix traditional `pages` directory SSR with App Router RSCs. You can embed RSC payloads within SSR pages, as the `RSC_HEADER` negotiation in [`packages/next/src/server/app-render/app-render.tsx`](https://github.com/vercel/next.js/blob/main/packages/next/src/server/app-render/app-render.tsx) handles the streaming separately from the HTML generation in [`packages/next/src/server/base-server.ts`](https://github.com/vercel/next.js/blob/main/packages/next/src/server/base-server.ts).

### Why does RSC reduce client bundle size compared to SSR?

In SSR, all modules imported by a page are bundled for the client even if only used for data fetching. RSC uses the Flight loader defined in [`packages/next/src/build/webpack/plugins/next-flight-plugin.ts`](https://github.com/vercel/next.js/blob/main/packages/next/src/build/webpack/plugins/next-flight-plugin.ts) to ensure [`.server.tsx`](https://github.com/vercel/next.js/blob/main/.server.tsx) files and their dependencies are excluded from client bundles, sending only the serialized result.

### How does error handling differ between SSR and RSC in production?

SSR errors in `getServerSideProps` or during `renderToHTML` abort the entire response, typically showing a generic error page. RSC errors are captured at the component level in [`app-render.tsx`](https://github.com/vercel/next.js/blob/main/app-render.tsx), allowing the server to embed error placeholders in the payload while streaming continues for other components.

### Is getServerSideProps deprecated in favor of React Server Components?

No. While RSC represents the future architecture for the App Router, `getServerSideProps` remains fully supported in the Pages Router for request-time data fetching that produces complete HTML documents. The Next.js source code maintains both `renderToHTML` in [`base-server.ts`](https://github.com/vercel/next.js/blob/main/base-server.ts) and the RSC pipeline in [`app-render.tsx`](https://github.com/vercel/next.js/blob/main/app-render.tsx) as first-class citizens.