Client, Server, or Edge: How to Render Smartly in Next.js
Choosing between client, server, or edge rendering in Next.js isn't about what's "better", it's about what makes sense for your data, UX, and context.
1. The Problem: Too Many Rendering Options
Modern Next.js App Router (13+) is powerful — and a bit overwhelming.
Now we deal with:
Server Components vs
use client
Static vs Dynamic rendering with
revalidate
and cache optionsRoute Handlers and Edge Runtime
Middleware for request interception
React Suspense, Streaming, Progressive Enhancement
So… where the hell should you render what?
2. Mental Models for Smarter Rendering
🧭 Model 1: Data Freshness vs Performance
🧭 Model 2: Where Does the Data Live?
Ask yourself: Where is the data I need coming from?
🧭 Model 3: Request vs Build Time
Build Time (Static):
Content doesn't change per user
Can be pre-generated
Examples: marketing pages, documentation
Request Time (Dynamic):
Needs user context (auth, preferences)
Data changes frequently
Examples: dashboards, user profiles
3. Real-World Examples
✅ Blog Post Page
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then(res => res.json())
return posts.map((post) => ({ slug: post.slug }))
}
export default async function BlogPost({ params }: { params: { slug: string } }) {
// This runs at build time for static generation
const post = await fetch(`https://api.example.com/posts/${params.slug}`, {
next: { revalidate: false } // Static - never revalidate
}).then(res => res.json())
return <article>{post.content}</article>
}
⚡ Instant load, great SEO, minimal JS
✅ Dashboard After Login
// app/dashboard/page.tsx
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
export default async function Dashboard() {
const session = cookies().get('session')
if (!session) {
redirect('/login')
}
// This runs on each request
const userData = await fetch(`https://api.example.com/user/${session.value}`, {
next: { revalidate: 0 } // Always fresh
}).then(res => res.json())
return <div>Welcome {userData.name}</div>
}
👤 Personalized and secure
✅ Search Page with Filters
// app/search/page.tsx
import { SearchResults } from './SearchResults'
export default async function SearchPage({ searchParams }: {
searchParams: { q?: string }
}) {
// Server-side initial render
const initialResults = searchParams.q
? await fetch(`https://api.example.com/search?q=${searchParams.q}`).then(res => res.json())
: []
return (
<div>
{/* Client component for interactive filtering */}
<SearchResults initialResults={initialResults} />
</div>
)
}
// app/search/SearchResults.tsx
'use client'
import { useSearchParams, useRouter } from 'next/navigation'
export function SearchResults({ initialResults }) {
const searchParams = useSearchParams()
const router = useRouter()
// Client-side filtering without full page reload
const handleFilter = (filter) => {
const params = new URLSearchParams(searchParams)
params.set('filter', filter)
router.push(`/search?${params.toString()}`)
}
return (
<div>
{/* Interactive filters */}
<button onClick={() => handleFilter('recent')}>Recent</button>
{/* Results */}
</div>
)
}
✅ No flickers, progressive enhancement
✅ A/B Testing by Location
// middleware.ts
import { NextRequest, NextResponse } from 'next/server'
export function middleware(request: NextRequest) {
const country = request.geo?.country || 'US'
const variant = country === 'US' ? 'A' : 'B'
const response = NextResponse.next()
response.cookies.set('ab-variant', variant)
return response
}
export const config = {
matcher: ['/landing/:path*']
}
⚡ Fast routing decisions without hitting your server
4. Realistic Benchmarks
Important: These numbers vary greatly based on network, region, and content size.
5. Common Mistakes
❌ Using
use client
everywhereIncreases bundle size unnecessarily
Loses SEO benefits of Server Components
❌ Fetching in Client Components what could be Server Components
Adds waterfall requests
Shows loading states unnecessarily
❌ Overcomplicating middleware
Keep it simple: geo, auth, redirects only
Heavy logic belongs in Route Handlers
❌ Not understanding hydration boundaries
Server Components don't hydrate
Only Client Components need JavaScript on the client
6. Golden Rules
Default to Server Components when possible
Use static rendering for content that doesn't change per user
Middleware is for request interception, not business logic
Client Components only when you need interactivity or browser APIs
Test on slow networks and devices
Measure actual user metrics, not synthetic tests
Conclusion
Stop overthinking it. Server Components by default, Client Components for interactivity, static when possible. Mix strategies within the same page. Measure real metrics, not synthetic ones.
The best Next.js apps don't use one rendering strategy — they use the right one for each piece.
Build fast, ship smart.