Next.js & React Server Components: A Practical Guide
React Server Components (RSC) represent the biggest shift in React's architecture since hooks. But most developers are still confused about when to use them, how they actually work, and what problems they solve.
This guide cuts through the abstraction to give you practical, actionable understanding of Server Components in Next.js 14+.
The Mental Model: Where Code Runs
Before RSC, all your React code ran in the browser. Components were shipped as JavaScript, downloaded by users, parsed, and executed to render the UI.
With Server Components, React can now run components on the server. The output—not the code—is sent to the browser. This is a fundamental shift in how React applications work.
Server Components:
- Execute on the server at request time (or build time for static content)
- Have direct access to server resources (databases, file system, environment variables)
- Never ship their code to the browser
- Cannot use browser APIs or React state
- Execute in the browser (with server-side rendering for initial HTML)
- Ship as JavaScript to the browser
- Can use all browser APIs and React interactivity features
- Are what you've been writing all along
In Next.js 13+, all components are Server Components by default. To make a component run on the client, you add "use client" at the top.
Why This Matters: The Performance Story
Let's make this concrete with an example.
The Old Way: Client Component
// Before: Everything runs in the browser
'use client';
import { marked } from 'marked'; // 65KB library
import { format } from 'date-fns'; // 75KB library
export function BlogPost({ slug }: { slug: string }) {
const [post, setPost] = useState(null);
useEffect(() => {
fetch(/api/posts/${slug})
.then(res => res.json())
.then(setPost);
}, [slug]);
if (!post) return ;
return (
{post.title}
);
}
What gets sent to the browser:
- The component code
- The marked library (65KB)
- The date-fns library (75KB)
- Loading states and spinners
- Download page shell
- Download JavaScript bundle (140KB+ just for this component)
- Parse and execute JavaScript
- Make API request for post data
- Wait for response
- Finally render the content
The New Way: Server Component
// After: Runs on the server, sends only HTML
import { marked } from 'marked';
import { format } from 'date-fns';
import { db } from '@/lib/db';
export async function BlogPost({ slug }: { slug: string }) {
const post = await db.posts.findUnique({ where: { slug } });
return (
{post.title}
);
}
What gets sent to the browser:
- Pre-rendered HTML
- Zero JavaScript for this component
- Server fetches data, runs markdown parsing, formats dates
- Browser receives complete HTML
- Content is immediately visible
When to Use Server Components
Use Server Components when:
1. Fetching Data
Server Components can fetch data directly without API routes or useEffect:
// Direct database access - no API layer needed
async function UserProfile({ userId }: { userId: string }) {
const user = await db.users.findUnique({
where: { id: userId },
include: { posts: true, followers: true }
});
return (
{user.name}
{user.followers.length} followers
);
}
Benefits:
- No loading states for initial render
- No waterfalls (fetch everything you need in one place)
- Direct database access without API overhead
- Sensitive queries never exposed to the client
2. Using Heavy Dependencies
Libraries that are only needed for rendering can stay on the server:
// These libraries never ship to the browser
import { Prism } from 'prismjs'; // Syntax highlighting
import { renderMath } from 'katex'; // Math rendering
import { processMarkdown } from 'remark'; // Markdown processing
import { generateOG } from 'satori'; // Image generation
async function Documentation({ slug }: { slug: string }) {
const doc = await getDoc(slug);
const highlighted = Prism.highlight(doc.code, 'typescript');
const rendered = processMarkdown(doc.content);
return {/ rendered content /} ;
}
3. Accessing Sensitive Resources
Server Components can access things that should never be exposed to browsers:
async function AdminDashboard() {
// Environment variables without NEXT_PUBLIC_ prefix
const apiKey = process.env.ADMIN_API_KEY;
// Server-only modules
const { readFile } = await import('fs/promises');
const config = await readFile('./config.json', 'utf-8');
// Internal services
const metrics = await internalMetricsService.getStats();
return ;
}
4. Static Content
Content that doesn't change based on user interaction:
// Perfect for Server Components
function Footer() {
return (
);
}
function ProductCard({ product }: { product: Product }) {
return (
{product.name}
{product.description}
{/ Interactive parts can be Client Components /}
);
}
When to Use Client Components
Add "use client" when you need:
1. Interactivity
Anything that responds to user input:
'use client';
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return (
);
}
2. Browser APIs
Accessing browser-specific features:
'use client';
import { useEffect, useState } from 'react';
export function LocationDisplay() {
const [location, setLocation] = useState(null);
useEffect(() => {
navigator.geolocation.getCurrentPosition(setLocation);
}, []);
if (!location) return
Getting location...
;
return (
Lat: {location.coords.latitude},
Long: {location.coords.longitude}
);
}
3. React State and Effects
Components using useState, useEffect, useReducer, or custom hooks with state:
'use client';
import { useState, useEffect } from 'react';
export function SearchInput({ onSearch }: { onSearch: (q: string) => void }) {
const [query, setQuery] = useState('');
useEffect(() => {
const timeout = setTimeout(() => onSearch(query), 300);
return () => clearTimeout(timeout);
}, [query, onSearch]);
return (
setQuery(e.target.value)}
placeholder="Search..."
/>
);
}
4. Event Handlers
Components with onClick, onChange, onSubmit, etc.:
'use client';
export function LikeButton({ postId }: { postId: string }) {
const [liked, setLiked] = useState(false);
async function handleLike() {
setLiked(!liked);
await fetch(/api/posts/${postId}/like, { method: 'POST' });
}
return (
);
}
The Composition Pattern: Mixing Server and Client
The real power comes from composing Server and Client Components together.
Key rule: Server Components can import and render Client Components, but Client Components cannot import Server Components.
Pattern 1: Server Parent, Client Children
// app/posts/[slug]/page.tsx (Server Component)
import { db } from '@/lib/db';
import { CommentSection } from './CommentSection'; // Client Component
import { LikeButton } from './LikeButton'; // Client Component
export default async function PostPage({ params }: { params: { slug: string } }) {
const post = await db.posts.findUnique({ where: { slug: params.slug } });
return (
{post.title}
{/ rendered content /}
{/ Interactive parts are Client Components /}
Pattern 2: Passing Server Data to Client Components
// Server Component fetches data
async function ProductPage({ id }: { id: string }) {
const product = await db.products.findUnique({ where: { id } });
return (
{product.name}
{/ Pass data as props to Client Component /}
);
}
// Client Component receives serializable data
'use client';
function AddToCart({ productId, price, inventory }: Props) {
const [quantity, setQuantity] = useState(1);
// Interactive logic here
}
Pattern 3: Children Slots
// Client Component can receive Server Components as children
'use client';
export function Modal({ children, isOpen, onClose }: Props) {
if (!isOpen) return null;
return (
e.stopPropagation()}>
{children} {/ Can be Server Component content! /}
);
}
// Usage in Server Component
async function Page() {
return (
{/ This works! /}
);
}
Common Mistakes and How to Avoid Them
Mistake 1: Making Everything a Client Component
// ❌ Don't do this
'use client';
export function ProductList() {
const [products, setProducts] = useState([]);
useEffect(() => {
fetch('/api/products').then(r => r.json()).then(setProducts);
}, []);
return products.map(p => );
}
// ✅ Do this instead
export async function ProductList() {
const products = await db.products.findMany();
return products.map(p => );
}
Mistake 2: Importing Server-Only Code in Client Components
// ❌ This will error at build time
'use client';
import { db } from '@/lib/db'; // Server-only!
// ✅ Fetch data in Server Component, pass to Client
async function Parent() {
const data = await db.query();
return ;
}
Mistake 3: Unnecessary Client Component Boundaries
// ❌ Entire component tree becomes client-side
'use client';
export function Page() {
const [state, setState] = useState();
return (
{/ Now client-side /}
{/ Now client-side /}
{/ Only this needs client /}
{/ Now client-side /}
);
}
// ✅ Push client boundary down
export function Page() {
return (
{/ Server Component /}
{/ Server Component /}
{/ 'use client' here only /}
{/ Server Component /}
);
}
Performance Implications
Bundle Size
Every library imported in a Client Component is shipped to the browser. With Server Components:
- markdown parsing libraries (remark, marked): 0 KB shipped
- syntax highlighting (prism, shiki): 0 KB shipped
- date formatting (date-fns, moment): 0 KB shipped
- PDF generation, image processing: 0 KB shipped
Time to Interactive
Server Components reduce JavaScript that must be downloaded, parsed, and executed before the page becomes interactive. For content-heavy pages, this can mean seconds faster interactivity.
Streaming and Suspense
Server Components work with React Suspense for streaming HTML:
import { Suspense } from 'react';
export default function Page() {
return (
{/ Rendered immediately /}
}>
{/ Streams in when ready /}
}>
{/ Streams independently /}
);
}
Decision Framework
When creating a new component, ask:
- Does it need interactivity? → Client Component
- Does it use browser APIs? → Client Component
- Does it need React state? → Client Component
- Otherwise → Keep it as Server Component (default)
- Server Component fetches data and renders static parts
- Client Component handles interactive parts, receives data as props
The Bottom Line
React Server Components aren't just a performance optimization—they're a new way of thinking about where code runs in your application.
The mental shift: Start with Server Components, add "use client" only where you need interactivity.
This default-server approach means:
- Less JavaScript shipped to browsers
- Direct access to server resources
- Simpler data fetching patterns
- Better performance by default
The framework does the hard work of coordinating server and client rendering. Your job is to understand the boundary and place your "use client" directives thoughtfully.
Master this, and you'll build faster applications with less complexity.
About the Author

Martin Brandvoll
Founder & Lead Consultant
Martin brings 10+ years of experience bridging business strategy and technical implementation. He specializes in helping SMBs leverage technology for sustainable growth.
View all articles by Martin Brandvoll →Related Articles
Website Speed: The Hidden Conversion Killer
Every second of load time costs you 7% in conversions. Here's how to fix it.

What Does a Website Cost in 2025?
A realistic breakdown of website costs in Norway, from simple one-pagers to complex web applications. Learn what affects the price and how to budget smartly.

Get insights delivered
Weekly articles on business strategy, technology, and building sustainable growth.
No spam. Unsubscribe anytime.