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.
Written by: Chia1104 CC BY-NC-SA 4.0
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.
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.
In this model, components can roughly be categorized into three types:
'use client' directive.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?)
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:
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 Type | Server Behavior | Client Behavior | Hydration |
|---|---|---|---|
| Server Components | Render and serialize into the RSC Payload | Not executed, JS not loaded | No hydration |
| Client Components | Pre-rendered to HTML | JS is downloaded and executed | Requires 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>
);
};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.
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.
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.
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.
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.
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>
)
}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
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.
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.
| Aspect | SSR | RSC |
|---|---|---|
| Nature | Rendering strategy (when/where HTML is rendered) | Component type (where components execute) |
| Output | A complete HTML page | RSC Payload + HTML (hybrid) |
| Client load | Must download full JS bundle for hydration | Server Components don't ship JS; only Client Components do |
| Hydration | All components require hydration | Only Client Components require hydration |
| Bundle size | Logic for all components is sent to the client | Server Components' code never reaches the client |
| Data fetching | Often done in getServerSideProps or outside components | Done directly inside components, more flexible |
| Streaming | Traditional SSR waits for full HTML | Native streaming support with incremental rendering |
| Interactivity delay | Hydration delay for every component | Hydration delay only for Client Components |
RSC and SSR are not mutually exclusive; they can be combined. In fact, the Next.js App Router uses both:
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.
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.
In the pages architecture, we see _app.tsx, which receives AppProps.pageProps:
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).
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:
const Feeds = async () => {
const feeds = await getFeeds();
return (
// ...
)
}
const App = () => {
return (
<React.Suspense fallback={<p>Loading</p>}>
<Feeds />
</React.Suspense>
)
}
export default appNext.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.