Next.js Server vs Client Components
Overview & Context
When I transitioned from classic React SPAs to the Next.js App Router, understanding the boundary between Server Components and Client Components was one of the biggest learning curves. In Next.js, components run on the server by default. Based on my hands-on experience building web applications and this portfolio, here is a practical, beginner-to-intermediate breakdown of how both component types work and how I decide between them in real projects.
What I Learned: Server vs Client Components at a Glance
In the Next.js App Router, every component is a Server Component by default unless you explicitly add the "use client" directive at the top of the file.
• Server Components (Default): These execute on the server at build-time (SSG) or request-time (SSR). Because their JavaScript logic is not shipped to the browser, they keep bundle sizes light and improve page load speed. They are ideal for layout structures, static content, and direct data fetching.
• Client Components (marked with "use client"): These pre-render on the server as HTML and hydrate in the browser. They are required whenever you need client-side interactivity, state management, or browser APIs.
// By default, pages in the App Router are Server Components.
// Content rendering logic stays on the server with zero client bundle overhead.
import { notFound } from 'next/navigation'
import { getBlogPostBySlug } from '@/data/blog-posts'
interface PageProps {
params: Promise<{ slug: string }>
}
export default async function BlogPostPage({ params }: PageProps) {
const { slug } = await params
const post = getBlogPostBySlug(slug)
if (!post) notFound()
return (
<article className="max-w-3xl mx-auto py-10">
<h1 className="text-3xl font-bold text-white">{post.title}</h1>
<p className="text-sm text-gray-400 mt-2">{post.formattedDate} · {post.readingTime}</p>
<div className="mt-6 text-gray-300">{post.introduction}</div>
</article>
)
}A Practical Rule of Thumb for "use client"
When I first started, I used to mark entire pages with "use client". Over time, I realized a much better approach is keeping the page on the server and isolating client interactivity into small leaf components.
Here are the situations where I add "use client":
1. User Events: When a component handles onClick, onChange, or onSubmit events.
2. React State & Effects: When using useState(), useReducer(), or useEffect().
3. Browser-Only APIs: When accessing localStorage, window, navigator, or clipboard APIs.
'use client'
import { useState } from 'react'
import { Check, Copy } from 'lucide-react'
// Small, isolated leaf client component for clipboard interaction
export function CopyCodeButton({ code }: { code: string }) {
const [copied, setCopied] = useState(false)
const handleCopy = async () => {
await navigator.clipboard.writeText(code)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
return (
<button
type="button"
onClick={handleCopy}
className="p-1.5 rounded-lg border border-white/10 bg-white/5 text-gray-300 hover:text-white"
aria-label="Copy code to clipboard"
>
{copied ? <Check size={14} className="text-emerald-400" /> : <Copy size={14} />}
</button>
)
}Useful Composition Pattern: Passing Server Components as Children
A common scenario I encountered is wanting an animated container or client wrapper while keeping the inner content as Server Components. A clean solution is passing Server Components through the children prop of the Client Component.
'use client'
import { motion } from 'motion/react'
import type { ReactNode } from 'react'
export function Reveal({ children }: { children: ReactNode }) {
return (
<motion.div
initial={{ opacity: 0, y: 15 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.35 }}
>
{/* Children passed from a server page remain Server Components! */}
{children}
</motion.div>
)
}Production Application
Real-World Use Case: Building This Portfolio Website
Engineering Scenario
Building a modern portfolio with fast page loads, dark styling, smooth animations, and interactive features like copy buttons and mobile navigation.
Technical Implementation
I kept the main landing page and article readers as Server Components to ensure fast static delivery, while isolating interactive widgets (<CopyCodeButton />, <CustomCursor />, mobile navigation drawer) into targeted Client Components.
Architectural Impact
Helped maintain clean code organization, fast page transitions, and a responsive user experience without bundling unnecessary JavaScript.
Summary
Key Engineering Takeaways
- Keep components as Server Components by default to reduce browser bundle size and improve load times.
- Only use "use client" for components that actually need state, lifecycle hooks, or browser event listeners.
- Move "use client" boundaries down to small leaf components rather than making entire pages client-side.
- Pass Server Components as children into client animation wrappers to balance interactivity with performance.