React Server Components Deep Dive: Architecture, Patterns, and Performance
on React, Server components, Rsc, Next.js, Web performance, Javascript, Frontend architecture
React Server Components Deep Dive: Architecture, Patterns, and Performance
React Server Components (RSC) represent the most significant architectural change in React since Hooks. They fundamentally rethink where code runs, how data flows, and what gets sent to the browser.
This isn’t incremental improvement—it’s a paradigm shift.
Photo by James Harrison on Unsplash
The Mental Model
Before: Everything Ships to the Browser
// Traditional React - ALL of this goes to the browser
import { format } from 'date-fns'; // 72KB
import { marked } from 'marked'; // 47KB
import { highlight } from 'prism-react-renderer'; // 35KB
function BlogPost({ slug }) {
const [post, setPost] = useState(null);
useEffect(() => {
fetch(`/api/posts/${slug}`)
.then(r => r.json())
.then(setPost);
}, [slug]);
if (!post) return <Skeleton />;
return (
<article>
<h1>{post.title}</h1>
<time>{format(post.date, 'PPP')}</time>
<div dangerouslySetInnerHTML= />
</article>
);
}
Client bundle: 150KB+ just for libraries.
After: Server Does the Heavy Lifting
// Server Component - runs on server, sends HTML
import { format } from 'date-fns';
import { marked } from 'marked';
import { highlight } from 'prism-react-renderer';
import { db } from '@/lib/db';
async function BlogPost({ slug }) {
const post = await db.posts.findUnique({ where: { slug } });
if (!post) return notFound();
const html = highlight(marked(post.content));
return (
<article>
<h1>{post.title}</h1>
<time>{format(post.date, 'PPP')}</time>
<div dangerouslySetInnerHTML= />
<LikeButton postId={post.id} /> {/* Client Component */}
</article>
);
}
Client bundle: Only the LikeButton code ships to browser.
Server vs Client Components
Quick Reference
| Feature | Server Component | Client Component |
|---|---|---|
async/await | ✅ | ❌ |
useState/useEffect | ❌ | ✅ |
| Browser APIs | ❌ | ✅ |
| Event handlers | ❌ | ✅ |
| Direct DB access | ✅ | ❌ |
| Secrets/env vars | ✅ | ❌ (exposed) |
| Large dependencies | ✅ (no bundle cost) | ❌ (adds to bundle) |
The Boundary Pattern
// Server Component (default in App Router)
async function ProductPage({ id }) {
const product = await getProduct(id);
return (
<div>
<ProductDetails product={product} />
{/* This boundary switches to client */}
<AddToCartButton productId={id} />
</div>
);
}
// ProductDetails is also a Server Component
function ProductDetails({ product }) {
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
{/* Format price on server, no library sent to client */}
<span>{formatCurrency(product.price)}</span>
</div>
);
}
// Client Component - needs interactivity
'use client';
import { useState } from 'react';
import { addToCart } from '@/actions/cart';
function AddToCartButton({ productId }) {
const [pending, setPending] = useState(false);
async function handleClick() {
setPending(true);
await addToCart(productId);
setPending(false);
}
return (
<button onClick={handleClick} disabled={pending}>
{pending ? 'Adding...' : 'Add to Cart'}
</button>
);
}
Photo by Lautaro Andreani on Unsplash
Data Fetching Patterns
Parallel Data Fetching
// ❌ Sequential - slow
async function Dashboard() {
const user = await getUser();
const posts = await getPosts(user.id); // Waits for user
const analytics = await getAnalytics(user.id); // Waits for posts
return <DashboardView user={user} posts={posts} analytics={analytics} />;
}
// ✅ Parallel - fast
async function Dashboard() {
const user = await getUser();
// Start both at the same time
const [posts, analytics] = await Promise.all([
getPosts(user.id),
getAnalytics(user.id)
]);
return <DashboardView user={user} posts={posts} analytics={analytics} />;
}
Streaming with Suspense
import { Suspense } from 'react';
async function Page() {
return (
<div>
{/* Renders immediately */}
<Header />
{/* Streams in when ready */}
<Suspense fallback={<PostsSkeleton />}>
<Posts />
</Suspense>
{/* Streams independently */}
<Suspense fallback={<CommentsSkeleton />}>
<Comments />
</Suspense>
</div>
);
}
// Each component can be slow independently
async function Posts() {
const posts = await fetchPosts(); // 200ms
return <PostList posts={posts} />;
}
async function Comments() {
const comments = await fetchComments(); // 500ms
return <CommentList comments={comments} />;
}
Preloading Data
// Initiate fetch before component renders
import { preload } from 'react-dom';
import { getUser, getPosts } from '@/lib/data';
function Page({ userId }) {
// Start fetching immediately
preload(getUser, userId);
preload(getPosts, userId);
return (
<Suspense fallback={<Loading />}>
<UserProfile userId={userId} />
</Suspense>
);
}
Server Actions: Mutations Made Simple
// actions.ts
'use server';
import { revalidatePath } from 'next/cache';
import { db } from '@/lib/db';
export async function createPost(formData: FormData) {
const title = formData.get('title');
const content = formData.get('content');
// Direct database access - no API route needed
await db.posts.create({
data: { title, content, authorId: getCurrentUser().id }
});
// Revalidate cached data
revalidatePath('/posts');
}
export async function likePost(postId: string) {
await db.likes.create({
data: { postId, userId: getCurrentUser().id }
});
revalidatePath(`/posts/${postId}`);
}
// Using in components
'use client';
import { createPost, likePost } from '@/actions';
import { useActionState } from 'react';
function CreatePostForm() {
const [state, formAction, pending] = useActionState(createPost);
return (
<form action={formAction}>
<input name="title" required />
<textarea name="content" required />
<button disabled={pending}>
{pending ? 'Creating...' : 'Create Post'}
</button>
</form>
);
}
function LikeButton({ postId }) {
const likeWithId = likePost.bind(null, postId);
return (
<form action={likeWithId}>
<button type="submit">❤️ Like</button>
</form>
);
}
Composition Patterns
Passing Server Components as Props
// Server Component wrapper
async function DataProvider({ children }) {
const data = await fetchExpensiveData();
return (
<DataContext.Provider value={data}>
{children}
</DataContext.Provider>
);
}
// Can render Client Components inside
function Page() {
return (
<DataProvider>
<InteractiveWidget /> {/* Client Component */}
</DataProvider>
);
}
The Slot Pattern
// Server Component with client "slots"
async function ProductCard({ product, actions }) {
const details = await getProductDetails(product.id);
return (
<div className="card">
<img src={details.image} alt={product.name} />
<h2>{product.name}</h2>
<p>{details.description}</p>
{/* Client Component passed as prop */}
{actions}
</div>
);
}
// Usage
<ProductCard
product={product}
actions={<AddToCartButton productId={product.id} />}
/>
Caching Strategies
Request Memoization
// Same request in multiple components = one fetch
async function Layout({ children }) {
const user = await getUser(); // Fetch #1
return (
<div>
<Header user={user} />
{children}
</div>
);
}
async function Header({ user }) {
const user = await getUser(); // Deduplicated! Uses cached result
return <nav>{user.name}</nav>;
}
Static and Dynamic Rendering
// Force dynamic (no caching)
export const dynamic = 'force-dynamic';
// Or per-request
import { unstable_noStore as noStore } from 'next/cache';
async function LiveData() {
noStore(); // Always fresh
const data = await fetchLiveData();
return <div>{data}</div>;
}
// Time-based revalidation
async function Posts() {
const posts = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 } // Revalidate every hour
});
return <PostList posts={posts} />;
}
Performance Impact
Real-world metrics from production migrations:
| Metric | CSR | SSR | RSC |
|---|---|---|---|
| FCP | 2.1s | 1.2s | 0.8s |
| LCP | 3.5s | 1.8s | 1.1s |
| TTI | 4.2s | 3.1s | 1.4s |
| JS Bundle | 450KB | 450KB | 120KB |
Common Mistakes
1. Over-using ‘use client’
// ❌ Marks entire subtree as client
'use client';
function Page() {
return (
<div>
<StaticContent /> {/* Now also client! */}
<InteractiveButton />
</div>
);
}
// ✅ Only mark what needs interactivity
function Page() {
return (
<div>
<StaticContent /> {/* Server Component */}
<InteractiveButton /> {/* Has 'use client' */}
</div>
);
}
2. Serialization Boundaries
// ❌ Can't pass functions to Client Components
async function Page() {
const handleClick = () => console.log('clicked');
return <Button onClick={handleClick} />; // Error!
}
// ✅ Use Server Actions instead
async function Page() {
async function handleSubmit() {
'use server';
console.log('submitted');
}
return <Form action={handleSubmit} />;
}
Conclusion
React Server Components are not just an optimization—they’re a fundamental rethinking of where React code runs. By moving rendering to the server where possible:
- Bundle sizes shrink dramatically
- Data fetching simplifies
- Sensitive logic stays secure
- Performance improves by default
The learning curve is real, but the benefits are substantial. Start by identifying components that don’t need interactivity, and gradually migrate them to Server Components.
The future of React is hybrid, and RSC is how we get there.
Have you migrated to Server Components? Share your experience in the comments.
이 글이 도움이 되셨다면 공감 및 광고 클릭을 부탁드립니다 :)
