Here's what confuses every Next.js developer in 2025:
"Should I use Server Components or Client Components?"
App Router changed everything. Some articles say "use Server Components everywhere." Others say "it's over-hyped."
Let me give you clear answer: It depends on your use case.
This guide shows you exactly when to use each—with real examples for 2025.
App Router vs Pages Router: What Changed
Let's understand the shift first.
Pages Router (Next.js 12 and earlier)
// pages/index.jsexport default function Home() {return <div>Hello World</div>;}
Characteristics:
- Everything is Client Component by default
getServerSideProps,getStaticPropsfor data fetching- Server rendering but less control
- More client JavaScript sent to browser
App Router (Next.js 14+)
// app/page.jsexport default function Home() {return <div>Hello World</div>;}
Characteristics:
- Server Components by default (no client JS sent)
async/awaitfor data fetching (simpler)- Nested layouts and routing
- Streaming support
- Better performance (smaller bundles)
The Key Difference: App Router sends zero JavaScript to browser by default. Pages Router sends everything.
Server Components: Use by Default
Server Components are the biggest win in Next.js 14.
What Are Server Components?
- Render on server (not in browser)
- Zero client JavaScript for component itself
- Direct database access (no API calls)
- Secret access (environment variables safe)
- Automatic streaming for slow data
Performance Gain: 30-50% smaller client bundles, faster initial loads
When to Use Server Components
✅ Use For:
- Data fetching (databases, APIs)
- Components that don't need interactivity
- SEO-critical content
- Heavy computations
- Sensitive data (can't leak to client)
Server Component Example
// app/products/page.jsasync function getProducts() {// Direct database access (no API!)const products = await db.product.findMany();return products;}// Server Component by defaultexport default async function ProductsPage() {const products = await getProducts();return (<div>{products.map((product) => (<ProductCard key={product.id} product={product} />))}</div>);}
What Happens:
- Query runs on server
- HTML sent to browser with products rendered
- Zero JavaScript sent for this component
- Client sees content instantly
Client Components: Use When Needed
Client Components are for interactivity.
What Are Client Components?
- Render in browser
- Full access to browser APIs
- State and effects allowed
- 'use client' directive required
When to Use Client Components
✅ Use For:
- Interactivity (onClick, onChange, etc.)
- Browser APIs (window, localStorage, etc.)
- State (useState, useEffect)
- Third-party libraries that require browser
- Real-time features (WebSockets, etc.)
Client Component Example
// app/components/Counter.js"use client"; // Required!import { useState } from "react";export default function Counter() {const [count, setCount] = useState(0);return (<div><button onClick={() => setCount(count + 1)}>Count: {count}</button></div>);}
What Happens:
- Component sends as JavaScript to browser
- Renders in browser (not server)
- Full React capabilities available
Mixing Server and Client Components
Real apps need both. Here's how to mix them properly.
Architecture: Server Parent, Client Children
// app/users/page.js (Server Component)async function getUsers() {return await db.user.findMany();}export default async function UsersPage() {const users = await getUsers();return (<div><h1>Users</h1>{/* Client child for interactivity */}<UserTable users={users} /></div>);}// app/components/UserTable.js (Client Component)'use client';export default function UserTable({ users }) {const [sortConfig, setSortConfig] = useState({ column: null, direction: 'asc' });const [filter, setFilter] = useState('');const sortedUsers = useMemo(() => {if (!sortConfig.column) return users;return [...users].sort((a, b) => {const multiplier = sortConfig.direction === 'asc' ? 1 : -1;return a[sortConfig.column] > b[sortConfig.column] ? multiplier : -multiplier;});}, [users, sortConfig]);return (| setSortConfig({ column: 'name', direction: sortConfig.direction === 'asc' ? 'desc' : 'asc' })}> Name | setSortConfig({ column: 'email', direction: sortConfig.direction === 'asc' ? 'desc' : 'asc' })}> Email || --- | --- || {user.name} | {user.email} |);}
What Happens:
UsersPage(server) fetches data, renders static HTMLUserTable(client) hydrates in browser, adds interactivity- Server JavaScript = 0, Client JavaScript = small bundle
Data Fetching in App Router
App Router simplifies data fetching significantly.
Fetching in Server Components
// app/dashboard/page.jsasync function getDashboardData() {const [user, stats, notifications] = await Promise.all([db.user.findUnique({ where: { id: userId } }),db.stats.findMany({ where: { userId } }),db.notification.findMany({ where: { userId, read: false } }),]);return { user, stats, notifications };}export default async function Dashboard() {const data = await getDashboardData();return (<div><UserStats stats={data.stats} /><Notifications notifications={data.notifications} /></div>);}
Fetching in Client Components
// app/components/RealtimeNotifications.js"use client";import { useEffect, useState } from "react";export default function RealtimeNotifications() {const [notifications, setNotifications] = useState([]);useEffect(() => {// Browser API: fetch in browserconst eventSource = new EventSource("/api/notifications");eventSource.onmessage = (event) => {const notification = JSON.parse(event.data);setNotifications((prev) => [...prev, notification]);};return () => eventSource.close();}, []);return <NotificationList notifications={notifications} />;}
Parallel Data Fetching
export default async function Page() {// Fetches in parallelconst [products, categories, recommendations] = await Promise.all([getProducts(),getCategories(),getRecommendations(),]);return (<PageContentproducts={products}categories={categories}recommendations={recommendations}/>);}
Routing in App Router
File-based routing is simpler and more powerful.
File-Based Routing Structure
app/├── layout.js # Root layout├── page.js # Home (/)├── about/│ └── page.js # About (/about)├── blog/│ ├── page.js # Blog index (/blog)│ ├── [slug]/ # Dynamic route (/blog/:slug)│ │ └── page.js│ └── category/│ └── [id]/ # Nested dynamic route (/blog/category/:id)│ └── page.js├── (group)/ # Route groups│ └── dashboard/│ └── page.js # /dashboard (without /group in URL)└── api/└── route.js # API routes
Dynamic Routes
// app/blog/[slug]/page.jsexport default async function BlogPost({ params }) {const { slug } = params;const post = await db.post.findUnique({ where: { slug } });if (!post) {notFound();}return <PostContent post={post} />;}
Route Groups
// app/(marketing)/layout.jsexport default function MarketingLayout({ children }) {return (<div className="marketing-layout">{children}</div>);}// app/(marketing)/about/page.jsexport default function About() {return <div>About page (inside marketing layout)</div>;}// app/(dashboard)/layout.jsexport default function DashboardLayout({ children }) {return (<div className="dashboard-layout">{children}</div>);}
Streaming: The Performance Superpower
App Router supports React Server Components streaming.
What Is Streaming?
Server sends HTML in chunks as it's generated. Browser renders each chunk immediately.
Without Streaming:
[Wait 3 seconds for entire page...]
[Display entire page at once]
With Streaming:
[Display header immediately]
[Display hero after 0.5s]
[Display content after 1s]
[Display sidebar after 2s]
Performance Gain: Perceived load time 50-80% faster even if total time is same.
Automatic Streaming
// app/slow-page/page.jsexport default async function SlowPage() {// These fetch in parallel and stream as readyconst [header, content, footer] = await Promise.all([getHeader(), // Takes 0.5sgetContent(), // Takes 2sgetFooter(), // Takes 0.3s]);return (<><header data={header} /><content data={content} /><footer data={footer} /></>);}
What Happens:
- Header streams immediately (0.5s)
- Content streams when ready (2s)
- Footer streams when ready (0.3s)
- User sees progress, not blank screen
Server Actions: Form Handling Simplified
Server Actions make form handling secure and simple.
Server Action Example
// app/actions.js"use server";import { revalidatePath } from "next/cache";import { redirect } from "next/navigation";export async function createPost(formData) {// Validate inputconst title = formData.get("title");const content = formData.get("content");if (!title || !content) {return { error: "Title and content are required" };}// Direct database accessconst post = await db.post.create({data: { title, content },});// Revalidate cacherevalidatePath("/");// Redirectredirect(`/blog/${post.slug}`);}
Use in Component
// app/blog/new/page.jsimport { createPost } from "../actions";export default function NewPostPage() {return (<form action={createPost}><input name="title" placeholder="Title" /><textarea name="content" placeholder="Content" /><button type="submit">Create Post</button></form>);}
Benefits:
- No client-side validation needed (happens on server)
- No API routes to create
- Direct database access
- Automatic form validation
- Progressives enhancement (works without JS)
Caching in App Router
App Router has built-in caching.
Request Deduplication
// Same component called multiple timesexport default async function Page() {// This runs ONCE per requestconst data = await getData();return <Content data={data} />;}
Full Route Cache
// app/page.jsexport const revalidate = 3600; // Revalidate every hourexport default async function Page() {const data = await fetchData(); // Cached for hourreturn <Content data={data} />;}
On-Demand Revalidation
// app/api/create-post/route.jsimport { revalidatePath } from "next/cache";export async function POST(req) {const post = await createPost(req.body);// Revalidate specific pagerevalidatePath("/blog");return Response.json({ success: true });}
App Router Best Practices
1. Server Components First
Start with Server Component. Add 'use client' only when needed.
// Good: Server by defaultexport default function Page() {return <div>{data}</div>;}// Add client only when interactivity needed'use client';export default function Interactive() {const [state, setState] = useState();return <button onClick={() => setState(!state)}>{state ? 'On' : 'Off'}</button>;}
2. Keep Client Components Small
Split large client components into smaller pieces.
// Bad: Everything in one client component"use client";export default function HugeComponent() {// 500 lines of state, effects, hooks}// Good: Split into focused components// UserProfile.js (client)// UserSettings.js (client)// UserNotifications.js (client)
3. Move State Down
Keep state as low in tree as possible.
// Bad: State in parent, passed through 3 levels<Parent state={state}><Child1 state={state}><Child2 state={state} /></Child1></Parent>// Good: State where needed<Parent><Child1 /><Child2 /></Parent>
4. Use Loading and Streaming
Don't block entire page on one slow request.
export default async function Page() {return (<Suspense fallback={<Skeleton />}><SlowComponent /></Suspense>);}
5. Optimize Images
Use next/image for automatic optimization.
import Image from "next/image";export default function Page() {return (<Imagesrc="/hero.jpg"alt="Hero image"width={1200}height={630}priority/>);}
Migration Guide: Pages to App Router
Step 1: Create app/ directory
mkdir app
Step 2: Move pages to app structure
# Beforepages/index.jspages/about.jspages/blog/[slug].js# Afterapp/page.jsapp/about/page.jsapp/blog/[slug]/page.js
Step 3: Remove getServerSideProps/getStaticProps
// Before (pages router)export async function getServerSideProps() {return { props: { data } };}// After (app router)export default async function Page() {const data = await getData();return <PageContent data={data} />;}
Step 4: Update imports and routing
// Beforeimport Link from "next/link";import { useRouter } from "next/router";// Afterimport Link from "next/link";import { useRouter } from "next/navigation";
Step 5: Test thoroughly
- Test all pages render correctly
- Test data fetching works
- Test client components have interactivity
- Test routing works
- Test forms submit correctly
Common App Router Mistakes
1. Adding 'use client' everywhere
Mistake: Marking everything as client component
Reality: Defeats purpose of App Router (performance)
Fix: Use 'use client' only when interactivity needed
2. Fetching data in useEffect in Server Components
Mistake:
export default function Page() {const [data, setData] = useState();useEffect(() => {fetchData().then(setData);}, []);return <div>{data}</div>;}
Reality: Extra round trip, doesn't use streaming
Fix:
export default async function Page() {const data = await fetchData();return <div>{data}</div>;}
3. Not using Suspense for streaming
Mistake: No streaming, page waits for everything
Reality: Worse perceived performance
Fix:
export default function Page() {return (<Suspense fallback={<Loading />}><SlowComponent /></Suspense>);}
Related Reading
If you found this helpful, you might also enjoy:
- State Management in React: Complete Guide - React state
- Frontend Testing Guide for Startups - Test your app
- React Component Architecture Patterns - Component structure
- Next.js vs React: When to Use Each - Framework choice
Quick Takeaways
- Server Components by default—zero JavaScript sent to browser, direct database access, better performance (30-50% smaller bundles)
- Use Client Components only when needed—interactivity, browser APIs, state management, third-party libraries requiring browser
- Data fetching is simpler—use async/await in Server Components, no more getServerSideProps/getStaticProps
- Streaming improves perceived performance—pages render progressively, users see content immediately instead of blank screens
- Server Actions replace API routes—handle form submissions directly in components with "use server" directive
- Migration path: Create app/ directory → Move pages to app structure → Remove data fetching methods → Update imports → Test thoroughly
- Common mistakes to avoid: Adding 'use client' everywhere, fetching data in useEffect in Server Components, not using Suspense for streaming
Frequently Asked Questions
Should I migrate from Pages Router to App Router?
If you're starting a new project, use App Router. For existing projects, migrate if you need Server Components, better performance, or simpler data fetching. Migration requires refactoring but provides long-term benefits.
What's the biggest benefit of App Router?
Server Components by default. They render on the server with zero client JavaScript, enabling direct database access, reduced bundle sizes, and automatic streaming. This means faster initial page loads.
Can I mix Server and Client Components?
Yes, and you should. Use Server Components for data fetching and static content, Client Components for interactivity. Pass data from Server to Client Components via props.
Do I need to learn new APIs for App Router?
The core React APIs are the same. Main changes: async components for data fetching, new routing conventions, Server Actions for mutations, and the 'use client' directive. Most concepts transfer from Pages Router.
Is App Router stable for production?
Yes. App Router has been stable since Next.js 13.4 and is the recommended approach for new Next.js applications. It's used in production by thousands of companies.
References and Sources
-
Next.js App Router Documentation - Official Next.js 14/15 App Router guide.
-
React Server Components RFC - Technical specification for Server Components architecture.
-
Vercel App Router Examples - Production patterns and implementation examples.
-
Next.js Conf 2025 - Latest App Router features and best practices presentations.
Word Count: ~4,800 words
Need Help with Next.js App Router?
At Startupbricks, we've built dozens of Next.js apps with App Router. We know common pitfalls, performance optimizations, and best practices.
Whether you need:
- Full Next.js application build
- Migration from Pages to App Router
- Performance optimization
- Server component architecture
Let's talk about building with App Router.
Ready for App Router? Download our free App Router Checklist and start today.
