React Server Components: A Practical Guide
Understanding when and how to use RSC in Next.js 15 for better performance, with real-world examples and common pitfalls.
Maulik Joshi
Full-Stack Developer
React Server Components (RSC) represent the most significant shift in how we build React applications since hooks. They fundamentally change the rendering model — allowing components to execute on the server, reducing client-side JavaScript, and enabling direct database access from your UI layer. Let's break down what this means in practice.
The Mental Model Shift
Before RSC, every React component shipped to the client. Even if a component just rendered static text, its JavaScript was bundled, downloaded, parsed, and executed on the user's device. Server Components flip this: they render on the server and send only the HTML result — zero JavaScript for that component reaches the client.
In Next.js App Router, all components are Server Components by default. You opt INTO client rendering with "use client", not the other way around.
Server vs Client: When to Use What
Here's the decision framework I use on every project:
Practical Example: Blog Post Page
Let's build a blog post page that demonstrates the RSC pattern. The page itself is a Server Component that fetches data, while interactive elements are extracted into small Client Components:
The Client Components
Notice how we push interactivity to the leaf nodes. The LikeButton is a tiny Client Component:
The Composition Pattern
The most powerful RSC pattern is composition — passing Server Components as children to Client Components. This lets you keep server-rendered content inside interactive wrappers:
Think of the "use client" boundary like a waterfall: once you mark a component as client, all its imports become client too. Push client boundaries as deep (leaf-ward) as possible.
Common Pitfalls
1. Passing Non-Serializable Props
Server Components can pass props to Client Components, but those props must be serializable (JSON-safe). You can't pass functions, Dates, Maps, or class instances across the boundary:
2. Unnecessary "use client"
The most common mistake is adding "use client" to components that don't need it. Every time you add it, you're opting that entire subtree out of server rendering and adding to the client bundle.
Audit your "use client" directives regularly. If a component only uses props and renders JSX (no state, no effects, no event handlers), remove "use client" — it's a Server Component.
Performance Impact
Here's what we measured when migrating a medium-sized Next.js app from Pages Router (all client) to App Router with proper RSC usage:
- Client JS bundle: 340KB → 120KB (65% reduction)
- LCP: 2.8s → 1.4s on 3G
- Time to Interactive: 3.2s → 1.8s
- Lighthouse Performance score: 72 → 96
- Server render time: negligible increase (~50ms)
The goal isn't to make everything a Server Component. The goal is to use the right tool at each level — server for data and static content, client for interactivity.
Key Takeaways
- Default to Server Components — only add "use client" when you need interactivity
- Push client boundaries to leaf components for minimal JS bundles
- Use the composition pattern to mix server and client rendering
- Only pass serializable data across the server/client boundary
- Server Actions replace most API routes for mutations
- Measure before and after — the performance gains are real and significant
React Server Components aren't just a performance optimization — they're a better way to think about building web applications. By default, your app ships less JavaScript, loads faster, and is easier to reason about. The initial learning curve is worth it.
Stay updated with new posts
Get notified when I publish new articles. No spam, unsubscribe anytime.