Back in early 2023 a client of mine, a fintech startup based out of Canary Wharf, handed me a Next.js 13 codebase that a previous agency had built. It was using the new app/ directory. Nice. Except the hydration errors were everywhere, the bundle was 340 KB gzipped, and nobody on the old team could explain why they'd put "use client" on literally every file. When I asked them about React Server Components, they said: "Oh yeah, we use those." They did not use those.
That's the problem with RSCs right now. Everyone claims to understand them. Almost nobody actually does. So let me give you the version I wish existed in early 2023.
What RSCs Actually Are (Not the Marketing Version)
React Server Components are components that run only on the server and never ship their JavaScript to the browser. Full stop.
Not "server-side rendering." Not "pre-rendering." Those things existed before RSCs and they work differently. With traditional SSR (what Next.js pages router does), your components render on the server to produce HTML, but then the same JavaScript ships to the client so React can "hydrate" the page, attach event listeners, and take over.
RSCs skip the second part entirely. The component runs on the server, renders, sends its output as a serialised payload to the client, and then... that's it. No JavaScript for that component ever lands in the browser.
The practical effect: if you have a <ProductDescription> component that uses a 40 KB markdown parser to render some text, and you make it a server component, that 40 KB never goes to the user. The parsed HTML does. That's it.
The React team's original RFC is actually worth reading if you want the full rationale. It's dense but honest.
The Mental Model That Finally Made It Click for Me
Think of your component tree as two separate worlds that happen to be stitched together.
World 1 (Server). Has full access to your database, your filesystem, your environment variables, your secrets. Cannot use useState , useEffect, or any browser APIs. Cannot attach event listeners.
World 2 (Client). Runs in the browser. Can use all the React hooks you know. Cannot talk directly to your database. Sends requests to APIs instead.
Before RSCs, every component lived in World 2, even if it was rendered on the server first. With RSCs, you can now explicitly place components in World 1. Those components can render World 2 components as children, passing them serialisable props. But World 2 components cannot render World 1 components. The boundary is one-directional.
Here's where people get confused: you don't add a directive to make something a server component. In the Next.js app/ directory, everything is a server component by default . You opt into the client with "use client". Backwards from what most people assume.
Seahawk had a project last year, a large e-commerce catalogue for a UK retailer, where we audited 60-odd components. Turns out about 40 of them had "use client" for no reason. Removing it cut the JavaScript bundle by roughly 28%. One afternoon's work.
What You Can and Can't Do (A Concrete Breakdown)
Server Components can:
- Fetch data directly with
async/await, nouseEffect, no loading states, justconst data = await db.query(...) - Import heavy server-only libraries (like gray-matter for frontmatter parsing, or PDF generators) without touching the client bundle
- Read from the filesystem using Node's
fsmodule - Access environment variables that you don't want exposed to the browser
- Pass data down to client components as props
Server Components cannot:
- Use
useStateoruseReducer - Use
useEffectoruseLayoutEffect - Attach event handlers (no
onClick, noonChange) - Use browser APIs (
window,document,localStorage) - Use React Context directly (though there are patterns to work around this)
Client Components can do everything above that Server Components can't, but:
- They cannot directly call your database
- They cannot use server-only packages
- Their code ships to the browser
The line isn't about performance alone. It's about where code runs and what it has access to. That framing is more useful than thinking in terms of "fast" vs "slow."
Data Fetching is Where RSCs Actually Shine
This is the part I genuinely love. Pre-RSCs, fetching data in a Next.js app usually meant one of: getServerSideProps , getStaticProps , a client-side useEffect call, or some combination of all three with a custom hook to manage loading state. Messy.
With RSCs, you just... fetch. Inside the component. At the top level.
`` async function ProductPage({ id }) { const product = await getProduct(id); // calls your DB directly return <ProductDetails product={product} />; } ``
No props drilling from a page-level function. No loading spinners for data that could have been ready on arrival. The component is async, it awaits what it needs, and it renders.
And here's where it gets genuinely interesting: because each server component can fetch its own data independently, you avoid the old "God component" problem where one top-level function had to orchestrate all data for a whole page. Components become self-contained. Parallel fetching happens naturally when you await Promise.all() or when sibling components fetch independently.
I used this pattern on a dashboard project for a logistics firm in Birmingham last spring. Each widget, shipment stats, delay alerts, cost summary, was its own async server component. No shared loading state, no prop drilling, no race conditions. The page went from a 2.1-second LCP to 0.9 seconds. Not because of RSCs alone, but RSCs made the right architecture much easier to reach.
The Rendering Pipeline (What Actually Happens)
When a user requests a page in a Next.js 14 app using the app/ router, here's the rough sequence:
- Next.js runs your server components on the server
- Those components produce a special format called the React Server Component Payload (RSC Payload), not raw HTML, but a serialised description of the UI tree
- Next.js uses that payload to generate the initial HTML (for the first paint)
- That HTML is sent to the browser
- The RSC Payload is also sent, and the client React runtime uses it to hydrate only the client components in the tree
- Client components get their JavaScript, hydrate, and become interactive
Step 5 is what makes RSCs different from plain SSR. The client doesn't re-render server components. It just uses the payload to fill in the picture and then focuses hydration effort on the interactive bits.
The Next.js docs on rendering explain the payload format better than most blog posts I've read, if you want to go deeper.
Where RSCs Break Down (Honest Criticism)
Right. Let's not pretend this is all sunshine.
The `"use client"` boundary cascades. The moment you put "use client" on a component, every component it imports also becomes client-side. If you're not careful, one onClick handler can pull a surprisingly large subtree into the client bundle. I've seen this bite junior devs on the team multiple times.
Context is painful. React Context doesn't work in server components. If you've built your app architecture around Context for theming, auth state, or global config, you'll need to restructure. There are workarounds (pass values down as props, use a client wrapper), but it's friction you didn't have before.
Debugging is harder. Server component errors don't show up in the browser console the same way. The error boundary behaviour is different. You need to check server logs. Not a dealbreaker, but if you're used to all errors living in DevTools, expect a learning curve.
Third-party libraries often aren't ready. Any library that uses hooks or browser APIs under the hood will break in a server component. You'll spend time wrapping things in "use client" files just to use a date picker or an animation library. The React ecosystem is catching up but as of 2024 it's still patchy.
Honestly, for a simple marketing site or a content blog, you might not need RSCs at all. If your team is comfortable with the pages router and your performance is fine, upgrading for RSC support alone isn't worth the pain. I've talked clients out of migrating more than once.
Practical Advice for Adopting RSCs on an Existing Project
If you're moving an existing Next.js app to the app/ directory, here's the order I'd follow:
- Start with leaf components. Components that display data but don't handle interaction are the easiest wins. Make them server components first.
- Identify your real interactivity surface. Usually it's smaller than you think. Forms, modals, dropdowns. That's your
"use client"territory. - Push `"use client"` as low as possible. The button that submits a form should be a client component. The form layout around it probably doesn't need to be.
- Audit your bundle with [@next/bundle-analyzer](https://www.npmjs.com/package/@next/bundle-analyzer). Run it before and after. If you're not seeing meaningful bundle reduction, you're probably still shipping too much to the client.
- Don't share server-only modules. Install server-only from npm and import it at the top of any module that should never reach the browser. It'll throw at build time if something tries to import it client-side.
The server-only package is a small thing but it saved us from a pretty embarrassing data leak scenario on a healthcare client project. A developer accidentally imported a database utility into a component that was later marked "use client". The package caught it at build time. Use it.
FAQ
Are React Server Components the same as SSR?
No. SSR (server-side rendering) renders components to HTML on the server but still ships the component JavaScript to the client for hydration. RSCs render on the server and never send their JavaScript to the browser at all. SSR and RSCs can work together, and in Next.js app/ directory, they do, but they solve different problems.
Do I need Next.js to use React Server Components?
Technically no, but practically yes for most teams. RSCs require a framework that handles the server infrastructure, routing, and the RSC payload pipeline. Next.js 13+ with the app/ directory is the most production-ready option right now. Remix has a different model. Rolling your own setup is possible but not a good use of your time unless you're building a framework yourself.
Can I mix server and client components in the same page?
Yes, and that's the whole point. A page might be a server component (fetches data, no JS shipped), contain a client component for a search input, which contains server components as children passed via children prop. The tree interleaves. Just remember: server components can render client components, but client components cannot render server components directly.
What happens to my existing custom hooks?
Custom hooks that use useState , useEffect, or browser APIs can only run in client components. You don't need to rewrite them, just be deliberate about which components use them. If a hook wraps a data-fetching call, consider whether that data could be fetched in a server component instead and passed as props. Often it can, and you end up with simpler code.
Is there a performance cost to the RSC Payload format?
There can be, particularly if you're passing large amounts of data through the payload. The RSC Payload is not the same as JSON, it's a custom format, but it still adds bytes to your response. For most apps this is negligible compared to the JS bundle savings. For apps with very large data sets being passed as props, profile it. Don't assume RSCs are always smaller on the wire.
---
RSCs are a genuine architectural shift, not just a new API. The mental model takes a while to settle. Give it that time. Build something small with the app/ directory before committing a big client project to it. I spent about three weeks on experiments before I trusted myself to ship RSC-based architecture to production, and I'd been writing React since 2016.
The hand waving will continue from people who haven't built anything real with it. Now at least you know enough to spot the difference.