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)
javascript// 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+)
javascript// 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
javascript// 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
javascript// 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
javascript// 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 (<table><thead><tr><th onClick={() => setSortConfig({ column: 'name', direction: sortConfig.direction === 'asc' ? 'desc' : 'asc' })}>Name</th><th onClick={() => setSortConfig({ column: 'email', direction: sortConfig.direction === 'asc' ? 'desc' : 'asc' })}></th></tr></thead><tbody><inputvalue={filter}onChange={e => setFilter(e.target.value)}placeholder="Filter users..."/>{sortedUsers.filter(user => user.name.toLowerCase().includes(filter.toLowerCase())).map(user => (<tr key={user.id}><td>{user.name}</td><td>{user.email}</td></tr>))}</tbody></table>);}
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
javascript// 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
javascript// 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
javascriptexport 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
textapp/├── 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
javascript// 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
javascript// 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
javascript// 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
javascript// 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
javascript// 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
javascript// Same component called multiple timesexport default async function Page() {// This runs ONCE per requestconst data = await getData();return <Content data={data} />;}
Full Route Cache
javascript// 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
javascript// 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.
javascript// 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.
javascript// 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.
javascript// 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.
javascriptexport default async function Page() {return (<Suspense fallback={<Skeleton />}><SlowComponent /></Suspense>);}
5. Optimize Images
Use next/image for automatic optimization.
javascriptimport 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
bashmkdir app
Step 2: Move pages to app structure
bash# Beforepages/index.jspages/about.jspages/blog/[slug].js# Afterapp/page.jsapp/about/page.jsapp/blog/[slug]/page.js
Step 3: Remove getServerSideProps/getStaticProps
javascript// 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
javascript// 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:
javascriptexport default function Page() {const [data, setData] = useState();useEffect(() => {fetchData().then(setData);}, []);return <div>{data}</div>;}
Reality: Extra round trip, doesn't use streaming
Fix:
javascriptexport 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:
javascriptexport 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
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.
