Server Components Aren't Just an Optimization
Most articles treat React Server Components (RSC) as a performance feature. They're not—they're an architectural shift in where your application logic lives.
Understanding this changes how you design everything.
The Core Distinction
// Server Component — runs on server, never shipped to browser
// app/users/page.tsx
async function UsersPage() {
// Direct DB access — no API needed
const users = await db.users.findMany({
where: { active: true },
orderBy: { createdAt: 'desc' },
});
return (
<div>
{users.map(user => (
<UserCard key={user.id} user={user} />
))}
</div>
);
}
// Client Component — runs in browser, can use hooks and events
'use client';
function UserCard({ user }: { user: User }) {
const [expanded, setExpanded] = useState(false);
return (
<div onClick={() => setExpanded(!expanded)}>
<h3>{user.name}</h3>
{expanded && <p>{user.email}</p>}
</div>
);
}
What You Can't Do in Server Components
// ❌ All of these fail in Server Components:
'use server'; // not needed, this isn't the directive
import { useState, useEffect, useCallback } from 'react'; // ❌
import { useRouter } from 'next/navigation'; // ❌
function MyComponent() {
const [count, setCount] = useState(0); // ❌ no hooks
return <button onClick={() => setCount(c => c + 1)}>{count}</button>; // ❌ no event handlers
}
Server Components are for data fetching and rendering. Client Components are for interactivity.
The Composition Pattern
// Server Component fetches data
async function ProductPage({ params }: { params: { id: string } }) {
const product = await db.products.findUnique({
where: { id: params.id },
include: { reviews: true, images: true },
});
if (!product) notFound();
return (
<div>
<ProductImages images={product.images} /> {/* Server Component */}
<ProductDetails product={product} /> {/* Server Component */}
<AddToCartButton productId={product.id} /> {/* Client Component */}
<ReviewList reviews={product.reviews} /> {/* Server Component */}
<WriteReview productId={product.id} /> {/* Client Component */}
</div>
);
}
Most of the page is Server Components. Only the interactive parts are Client Components. The result: less JavaScript shipped, faster initial load.
Data Fetching Patterns
Parallel Fetching (avoid waterfalls)
// ❌ Sequential — slow
async function Dashboard() {
const user = await getUser(); // 100ms
const orders = await getOrders(); // 100ms
const analytics = await getStats(); // 100ms
// Total: 300ms
}
// ✅ Parallel — fast
async function Dashboard() {
const [user, orders, analytics] = await Promise.all([
getUser(),
getOrders(),
getStats(),
]);
// Total: 100ms (limited by slowest)
}
Streaming with Suspense
import { Suspense } from 'react';
async function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
{/* Renders immediately */}
<QuickStats />
{/* Streams in as data loads */}
<Suspense fallback={<OrdersSkeleton />}>
<RecentOrders />
</Suspense>
<Suspense fallback={<ChartSkeleton />}>
<RevenueChart />
</Suspense>
</div>
);
}
Users see content progressively instead of waiting for the slowest query.
Caching in RSC
import { cache } from 'react';
// Deduplicate identical requests within a render
const getUser = cache(async (id: string) => {
return db.users.findUnique({ where: { id } });
});
// Called in multiple components — only one DB query
await getUser('123'); // hits DB
await getUser('123'); // returns cached result
// Next.js fetch caching
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 }, // revalidate every hour
});
return res.json();
}
When to Use Each
Server Components:
- Fetching data from DB or APIs
- Accessing server-only resources (env vars, secrets)
- Large dependencies you don't want in the client bundle
- Static or semi-static content
Client Components:
- Interactive UI (buttons, forms, toggles)
- Browser APIs (localStorage, geolocation)
- Third-party libraries that use browser APIs
- Real-time updates (WebSocket, SSE)
The Mental Model Shift
Old model: fetch data in API routes, pass to components via props or state.
New model: components fetch their own data directly. Server Components are closer to server-rendered templates; Client Components are islands of interactivity.
This eliminates a ton of API route boilerplate for data you only need on the server.
Next.js 14 App Router with RSC patterns, streaming, and optimized data fetching: Whoff Agents AI SaaS Starter Kit.
Top comments (0)