What Is a React Server Component (RSC), and How Does It Relate to SSR?

React Server Components (RSC) are a next‑generation React feature that moves part of rendering to the server, enabling streaming, smaller bundles, better performance, and tighter security in modern Next.js applications.

CChia1104
Posts9 minutes read

What Are React Server Components (RSC)?

As frontend applications grow in complexity, the React ecosystem continues to evolve to meet the needs of both developers and users. React Server Components (RSC) are an innovative feature introduced by the React team to improve application performance and developer experience. RSC allows developers to move part of the component rendering process to the server, thereby optimizing client-side performance and resource usage. With Next.js 15 making RSC the default architecture in 2024, 2025 is shaping up to be a crucial year for RSC becoming a standard primitive in the React ecosystem.

Core Concepts of RSC

  • Server-side rendered components: RSC allows some components to be rendered only on the server, so they do not need to be instantiated on the client, reducing client workload. After these components run on the server, they are sent to the client as a serialized React component tree called the React Server Component Payload (RSC Payload).

  • Stateless and streamable: RSC components are generally stateless, meaning they do not rely on client-side state management, and can be streamed to speed up rendering. With streaming, the client does not need to wait for all data to be ready before it can start rendering subsequent content.

  • Performance improvements: By moving heavy computation or data fetching to the server, RSC reduces client computation requirements and improves overall application performance.

How Do RSC Work and Render?

In this model, components can roughly be categorized into three types:

  • Server Components: Rendered on the server, can access databases or the file system, and can directly use server-side APIs.
  • Client Component: Rendered on the client, can use Hooks and Context for state management, and offer higher interactivity. These components must be explicitly marked with the 'use client' directive.
  • Shared Component: Components that do not use server-side or client-side specific APIs, and can run on either the server or client. When building apps with RSC, it is recommended to start from shared components by default and only convert them to specific types when needed.

In this model, RSC and Client Components can coexist and participate in a hybrid rendering strategy.

A quick reminder: RSC only does one thing — it renders React components on the server. Actual UI rendering still follows React's render model: it can be part of a full HTML document generated on the server together with Client Components (SSR), or it can participate in client-side updates alongside Client Components.(How are Server Components rendered?)

RSC Serialization and Streaming

Once RSC have been rendered on the server, the result is sent to the client as a serialized RSC Payload (with content-type set to text/x-component) via streaming. The RSC Payload contains:

  1. The rendered result of Server Components — component output that has already been processed on the server.
  2. Markers for Client Components — where Client Components should be inserted, plus references to the corresponding JavaScript files.
  3. Props passed to Client Components — data passed from Server Components to Client Components.

Thanks to streaming, React on the client does not need to wait for all data to be ready before it can start processing and displaying content.

1:"$Sreact.fragment"
2:I[66334,[],""]
3:I[56948,[],""]
6:I[25203,["1029","static/chunks/1029-c0bfd2a11c61893b.js","3599","static/chunks/3599-ccb90f71afb27819.js","3008","static/chunks/3008-fc47ddb25fe42f17.js","4418","static/chunks/4418-abf5f06038322eb3.js","1069","static/chunks/1069-67d4d50c882438ea.js","139","static/chunks/139-d532539c2c23eacb.js","4719","static/chunks/app/%5Blocale%5D/(blog)/%5Btype%5D/error-08e3b20720a5ffdf.js"],"default"]
8:I[99526,[],"OutletBoundary"]

Key concept: Server Components do not hydrate.

Only Client Components go through hydration to attach event listeners and enable interactivity on the client.

Component TypeServer BehaviorClient BehaviorHydration
Server ComponentsRender and serialize into the RSC PayloadNot executed, JS not loadedNo hydration
Client ComponentsPre-rendered to HTMLJS is downloaded and executedRequires hydration

Finally, the overall component tree will look something like this. While some components are still streaming (promises not yet resolved), you can use React.Suspense to show a fallback like <ClientLoading />. Server Components' data fetching APIs are tightly integrated with Suspense, enabling loading states and partial unlock of content before the entire stream completes.

const App = () => {
  return (
    <ClientLayout>
      <ServerAuthGuard> {/* <-- RSC */}
        <ClientHeader />
          <React.Suspense fallback={<ClientLoading />}>
            <ServerContent /> {/* <-- RSC */}
          </React.Suspense>
        <ClientFooter />
      </ServerAuthGuard> {/* <-- RSC */}
    </ClientLayout>
  );
};

What Benefits Does RSC Bring?

  • Reduced bundle size
  • Direct integration with server-side APIs
  • Improved security

Reduced Bundle Size

Because RSC are rendered on the server, what gets sent to the client is already the rendered output; the code and dependencies of Server Components are never shipped to the browser JS bundle. For example, if this component were rendered on the client, using papaparse would add about 263 kB to the bundle, but rendering it as a Server Component means you no longer need to worry about that library’s bundle size on the client.

analytics.tsx
import Papa from "papaparse"; 


export default async function Analytics() {
  const csv = await getAnalyticsCSV();
  const parsed = Papa.parse(csv); 


  return (
    <table>
      {/* parsed data render */}
    </table>
  )
}

This performance improvement translates into a noticeably better user experience in real-world applications. Server Components let you move most data fetching to the server so the client no longer needs to fire as many requests, and typical useEffect driven network waterfalls on the client can be eliminated.

Direct Use of Server-Side APIs

Previously, the client could only obtain server data via fetch, but with RSC you can directly access the database within a component, reducing client load time. This capability enables developers to perform database queries at the component level without having to create extra API endpoints.

users.tsx
export default function Users() {
  const users = await sql`SELECT * FROM users`


  return (
    <table>
      {/* users data render */}
    </table>
  )
}

Server Components can be defined as async functions, allowing you to await data fetching directly during the render phase. This pattern greatly simplifies data flow and improves the developer experience.

Improved Security

In the past, when accessing some third-party services, API keys often had to be hidden behind backend endpoints. Now you can keep that logic entirely inside RSC. This means sensitive credentials and keys never appear in client-side code.

openai-embeddings.tsx
import OpenAI from "openai";


const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY ?? "st-SECRETKEY",
});


// text-embedding-ada-002, text-embedding-3-small, text-embedding-3-large
const MODEL = "text-embedding-ada-002";


export const generateEmbedding = async (value: string) => {
  const input = value.replaceAll("\n", " ");


  const { data } = await openai.embeddings.create({
    model: MODEL,
    input,
  });


  return data[0]?.embedding;
};


const searchDb = async (query: string) => {
  const embedding = await generateEmbedding(query);
  if (!embedding) {
    throw new Error("Embedding generation failed");
  }
  const sqlEmbedding = pgvector.toSql(embedding);
  const results = await sql`SELECT * FROM documents ORDER BY embedding <=> ${sqlEmbedding} LIMIT 3`.values();
  return results;
};


export default function OpenaiEmbeddings({query}: {query: string}) {
  const data = await searchDb(query)


  return (
    <table>
      {/* data render */}
    </table>
  )
}

Challenges of RSC

Increased Development Complexity

Because RSC's serialization format and client-side streaming require support from the bundler, you cannot simply drop RSC into an existing SPA build pipeline. This is one reason the React team has worked closely with the Next.js team. Today, the fastest way to use RSC in production is to use Next.js's App Router.

If you don’t want to implement RSC via Next.js, you can refer to the React team's earlier RSC Demo

Relationship Between RSC and SSR (Server-Side Rendering)

Let's revisit the relationship between RSC and SSR. The short version: RSC and SSR are different concerns, but they can work together to improve the user experience.

Basics of SSR

Server-Side Rendering (SSR) means rendering the initial HTML for the entire React application on the server, then sending it to the client. Its advantages include:

  • Faster first paint: Users can see a fully rendered page more quickly, improving perceived initial load performance.

  • SEO friendliness: Since content is rendered on the server, search engines can more easily crawl and index the page.

Differences Between RSC and SSR

AspectSSRRSC
NatureRendering strategy (when/where HTML is rendered)Component type (where components execute)
OutputA complete HTML pageRSC Payload + HTML (hybrid)
Client loadMust download full JS bundle for hydrationServer Components don't ship JS; only Client Components do
HydrationAll components require hydrationOnly Client Components require hydration
Bundle sizeLogic for all components is sent to the clientServer Components' code never reaches the client
Data fetchingOften done in getServerSideProps or outside componentsDone directly inside components, more flexible
StreamingTraditional SSR waits for full HTMLNative streaming support with incremental rendering
Interactivity delayHydration delay for every componentHydration delay only for Client Components

How RSC and SSR Complement Each Other

RSC and SSR are not mutually exclusive; they can be combined. In fact, the Next.js App Router uses both:

  • SSR: Renders the overall page (including Server and Client Components) into HTML.
  • RSC: Server Components run only on the server and are never shipped as JS, while Client Components are hydrated for interactivity.

By combining RSC and SSR, developers can design more optimized architectures. For example, you can use SSR to render the overall page shell while using RSC for heavy or non-interactive components, achieving both high performance and rich user experience.

How Next.js Uses RSC to Improve Client Experience

With Next.js reaching version 15, the RSC-based App Router architecture has been stable for some time. Before RSC, in the pages architecture, you typically fetched data in each page using getServerSideProps, which meant the client could only see the UI after the data had fully loaded.

_app.tsx
_document.tsx
index.tsx

In the pages architecture, we see _app.tsx, which receives AppProps.pageProps:

_app.tsx
import type { AppProps } from "next/app";


const App = ({ Component, pageProps }: AppProps) => { 
  return (
    <Component {...pageProps} />
  );
};


export default App;

The pageProps here is the data returned from getServerSideProps in each page. On first load or when navigating to that page, getServerSideProps runs to fetch data, with no built-in caching and a response time that directly affects page transitions and First Contentful Paint (FCP).

pages/feeds/index.tsx
import { type GetServerSideProps, type NextPage } from "next";


type Props = {
  feeds: any
}


export const getServerSideProps: GetServerSideProps = async (context) => {
  const feeds = await getFeeds(context);


  if (!feeds) {
    return {
       props: {
        feeds: null
       },
    };
  }


  return {
    props: {
      feeds
    },
  };
};


const Page: NextPage<Props> = ({feeds}) => {
  return (
    // ...
  );
};


export default Page;

With the App Router and RSC, you can fetch feeds directly in a Server Component, and if data isn't ready yet, show a fallback via React.Suspense:

app.tsx
const Feeds = async () => {
  const feeds = await getFeeds();


  return (
    // ...
  )
}


const App = () => {
  return (
    <React.Suspense fallback={<p>Loading</p>}>
      <Feeds />
    </React.Suspense>
  )
}


export default app

Next.js can also cache the result of getFeeds(), so data can be available on the first load and reused across requests, which further helps SEO.

Many optimizations that previously required custom backend work (response time tuning, caching strategies, etc.) can now be partly handled through RSC and the React/Next.js infra, though this does make the React infrastructure more complex and will likely continue to evolve.

Written by: Chia1104 CC BY-NC-SA 4.0

Chia1104
©
Chia1104