React Server Components Performance
Optimize React Server Components for maximum performance in Next.js
React Server Components (RSC) run exclusively on the server, sending only the rendered HTML to the client. When used correctly, they dramatically reduce JavaScript bundle size and improve performance.
Server vs Client Components
// Server Component (default) - no "use client"
// ✅ Runs on server, zero JS sent to client
async function ProductList() {
const products = await db.query('SELECT * FROM products');
return (
<ul>
{products.map(p => <li key={p.id}>{p.name}</li>)}
</ul>
);
}
// Client Component - needs "use client"
// ❌ JS bundle sent to client
'use client';
function ProductList() {
const [products, setProducts] = useState([]);
useEffect(() => {
fetch('/api/products').then(r => r.json()).then(setProducts);
}, []);
return (/* ... */);
}Performance Benefits
| Aspect | Server Component | Client Component |
|---|---|---|
| JS Bundle | 0 KB | Component + dependencies |
| Data Fetching | Direct DB/API access | Needs API route |
| Hydration | None needed | Required |
| Time to Interactive | Faster | Slower |
Composition Patterns
Server Components Wrapping Client
import { ClientCounter } from './Counter';
export default async function Page() {
// Fetch data on server
const initialCount = await getCount();
// Pass to client component
return (
<div>
<h1>Dashboard</h1>
<ClientCounter initialCount={initialCount} />
</div>
);
}
// Counter.tsx
'use client';
export function ClientCounter({ initialCount }) {
const [count, setCount] = useState(initialCount);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}Minimizing Client Boundaries
// ❌ Bad: Entire page is client-side
'use client';
export default function ProductPage({ params }) {
const [product, setProduct] = useState(null);
// Everything here is client-side...
}
// ✅ Good: Only interactive parts are client
// ProductPage.tsx (Server Component)
export default async function ProductPage({ params }) {
const product = await getProduct(params.id);
return (
<div>
{/* Server-rendered, no JS */}
<ProductHeader product={product} />
<ProductDescription product={product} />
<ProductSpecs product={product} />
{/* Only this needs JS */}
<AddToCartButton productId={product.id} />
</div>
);
}Islands Architecture
// Large server-rendered page with small client islands
export default async function ArticlePage({ params }) {
const article = await getArticle(params.slug);
return (
<article>
{/* All static - no JS */}
<ArticleHeader title={article.title} author={article.author} />
<ArticleContent content={article.content} />
{/* Interactive islands */}
<Suspense fallback={<ShareButtonSkeleton />}>
<ShareButtons url={article.url} />
</Suspense>
<Suspense fallback={<CommentsSkeleton />}>
<Comments articleId={article.id} />
</Suspense>
</article>
);
}Data Fetching Patterns
Direct Database Access
// Server Component can access DB directly
import { db } from '@/lib/db';
async function UserProfile({ userId }) {
// No API route needed!
const user = await db.user.findUnique({
where: { id: userId },
include: { posts: true },
});
return (
<div>
<h1>{user.name}</h1>
<PostList posts={user.posts} />
</div>
);
}Parallel Data Fetching
async function Dashboard() {
// Parallel fetching - all start immediately
const [stats, users, revenue] = await Promise.all([
getStats(),
getUsers(),
getRevenue(),
]);
return (
<div className="grid grid-cols-3 gap-4">
<StatsCard stats={stats} />
<UsersCard users={users} />
<RevenueCard revenue={revenue} />
</div>
);
}Cached Data Fetching
import { cache } from 'react';
// Memoize expensive fetches
const getUser = cache(async (id: string) => {
const user = await db.user.findUnique({ where: { id } });
return user;
});
// Called multiple times, only fetches once
async function UserPage({ params }) {
const user = await getUser(params.id);
return (
<>
<UserHeader user={await getUser(params.id)} />
<UserPosts user={await getUser(params.id)} />
</>
);
}Avoiding Common Mistakes
Don't Import Client Libraries in Server Components
// ❌ Bad: Imports entire library to server
import _ from 'lodash';
async function ProductList() {
const products = await getProducts();
const sorted = _.sortBy(products, 'name');
return <ul>{/* ... */}</ul>;
}
// ✅ Good: Use native methods or server-safe utilities
async function ProductList() {
const products = await getProducts();
const sorted = products.sort((a, b) => a.name.localeCompare(b.name));
return <ul>{/* ... */}</ul>;
}Don't Pass Functions to Client Components
// ❌ Bad: Functions can't be serialized
async function Page() {
const handleClick = () => console.log('clicked');
return <ClientButton onClick={handleClick} />;
}
// ✅ Good: Use Server Actions
async function Page() {
async function handleClick() {
'use server';
console.log('clicked');
}
return <ClientButton onClick={handleClick} />;
}Don't Fetch in Client When Server Can
// ❌ Bad: Unnecessary client fetch
'use client';
function UserAvatar({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`).then(r => r.json()).then(setUser);
}, [userId]);
return user ? <img src={user.avatar} /> : null;
}
// ✅ Good: Fetch in server, pass data
// Server Component
async function UserAvatar({ userId }) {
const user = await getUser(userId);
return <img src={user.avatar} alt={user.name} />;
}Server Actions for Interactivity
'use server';
export async function addToCart(productId: string) {
const cart = await getCart();
await cart.add(productId);
revalidatePath('/cart');
}
// ProductPage.tsx (Server Component)
import { addToCart } from './actions';
export default async function ProductPage({ params }) {
const product = await getProduct(params.id);
return (
<div>
<h1>{product.name}</h1>
<form action={addToCart.bind(null, product.id)}>
<button type="submit">Add to Cart</button>
</form>
</div>
);
}Bundle Size Impact
| Approach | Client JS | When to Use |
|---|---|---|
| Server Component | 0 KB | Static content, data fetching |
| Client Component with chart library | 200+ KB | Interactive visualizations |
'use client';
import { Chart } from 'chart.js'; // Ships 200KB to client
export function RevenueChart() {
return <Chart type="bar" data={data} />;
}import { renderChartToSVG } from './chart-server';
async function RevenueChart() {
const data = await getRevenueData();
const svg = renderChartToSVG(data); // Rendered on server
return <div dangerouslySetInnerHTML={{ __html: svg }} />;
}Performance Checklist
Component Boundaries
Default to Server Components No "use client" means zero client JS.
Only add 'use client' when needed Required for hooks and event handlers only.
Keep client components small Extract interactive parts into separate components.
Push client boundary down the tree Move 'use client' as far down as possible.
Data Fetching
Fetch in Server Components Direct DB/API access without client round-trip.
Use parallel fetching Promise.all for independent data requests.
Implement request memoization Use React's cache() for deduplication.
Avoid waterfalls Don't await in sequence when parallel is possible.
Bundle Optimization
Avoid large client dependencies Heavy libraries should stay on server.
Use server-safe alternatives Native methods instead of lodash on server.
Check bundle analyzer Verify client bundle doesn't include server code.
Tree shake unused code Configure modularizeImports for heavy packages.