What is Server-Side Rendering in Next.js? | ZextOverse
What is Server-Side Rendering in Next.js?
Your page loads. The user sees content instantly — no spinner, no layout shift, no waiting for JavaScript to hydrate. That's the promise of Server-Side Rendering, and Next.js makes it remarkably approachable.
Before a user sees a single pixel of your application, a fundamental question must be answered: where does the HTML come from?
In traditional client-side rendering (CSR) — the default behavior of a plain React app — the answer is: the browser builds it. The server sends a nearly empty HTML shell, ships a large JavaScript bundle, the browser downloads and parses it, React runs, fetches data, and finally renders the UI. The user stares at a blank screen or skeleton loader the entire time.
Server-Side Rendering (SSR) inverts this model. The server receives the request, fetches the necessary data, renders the full HTML, and sends a complete page to the browser. The user sees real content immediately — before a single line of JavaScript has executed on their device.
Next.js has supported SSR since its earliest versions, and it remains one of the framework's most powerful and distinctive features.
How SSR Works: The Request Lifecycle
Understanding SSR means understanding what happens during a single HTTP request:
The key insight: the user sees content at step 4, not after hydration at step 7. For slow connections or low-powered devices, this difference is measured in seconds.
SSR in the Pages Router
In Next.js's classic Pages Router, SSR is opt-in per page via the getServerSideProps function. Any page that exports this function will be server-side rendered on every request.
// pages/dashboard.tsx
import type { GetServerSideProps, NextPage } from 'next'
interface Props {
user: {
name: string
plan: string
lastLogin: string
}
}
const Dashboard: NextPage<Props> = ({ user }) => {
return (
<main>
<h1>Welcome back, {user.name}</h1>
<p>Plan: {user.plan}</p>
<p>Last login: {user.lastLogin}</p>
</main>
)
}
export const getServerSideProps: GetServerSideProps = async (context) => {
// This code runs ONLY on the server — never in the browser
const { req, res, params, query } = context
const session = await getSession(req)
if (!session) {
return {
redirect: {
destination: '/login',
permanent: false,
},
}
}
const user = await db.users.findById(session.userId)
return {
props: {
user: {
name: user.name,
plan: user.plan,
lastLogin: user.lastLogin.toISOString(),
},
},
}
}
export default Dashboard
SPONSORED
InstaDoodle - AI Video Creator
Create elementAI Explainer Videos That Convert With Simple Text Prompts.
getServerSideProps runs on every request, not just at build time
It has access to req and res — you can read cookies, headers, and session data
It can redirect the user before the page renders — great for auth guards
Props must be JSON-serializable (no Date objects, class instances, etc.)
The component itself is completely unaware that data came from the server
SSR in the App Router
Next.js 13+ introduced the App Router with React Server Components (RSC), which changes the mental model considerably. In the App Router, all components are server components by default — meaning they render on the server without any special export needed.
// app/dashboard/page.tsx
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
// No special export needed — this IS a server component
export default async function DashboardPage() {
const cookieStore = cookies()
const session = await getSession(cookieStore)
if (!session) {
redirect('/login')
}
// Direct database access — this never runs in the browser
const user = await db.users.findById(session.userId)
const metrics = await db.metrics.getByUserId(session.userId)
return (
<main>
<h1>Welcome back, {user.name}</h1>
<MetricsGrid data={metrics} />
</main>
)
}
This is a profound shift. You're writing async/await directly in your component. You're calling your database directly. There's no API route in the middle, no useEffect, no loading state — the HTML that arrives in the browser already contains the rendered output.
To opt a component into client-side interactivity, you add 'use client' at the top:
The App Router model encourages a clear architectural boundary: server components handle data fetching and rendering, client components handle interactivity. Most of your component tree can remain on the server.
SSR vs. The Alternatives
Next.js offers several rendering strategies, and understanding when to use each is essential.
Static Site Generation (SSG)
Pages are rendered at build time. The HTML is generated once and served from a CDN on every request. Blazing fast, but the content is frozen until the next deployment.
Use when: Content doesn't change per-user or frequently (blog posts, documentation, marketing pages).
Incremental Static Regeneration (ISR)
A hybrid: pages are statically generated, but regenerated in the background after a configurable time interval. You get CDN-level performance with reasonably fresh content.
export const getStaticProps: GetStaticProps = async () => {
const data = await fetchProductCatalog()
return {
props: { data },
revalidate: 60, // Regenerate at most once per minute
}
}
Use when: Content is shared across users but changes periodically (product listings, news feeds).
Server-Side Rendering (SSR)
HTML is generated on the server on every request, with full access to request context.
Use when: Content is personalized per user, requires real-time data, or depends on cookies/headers (dashboards, authenticated pages, search results).
Client-Side Rendering (CSR)
The server sends a minimal HTML shell; the browser builds the UI after downloading and executing JavaScript.
Use when: The page is behind authentication (SEO irrelevant), content changes extremely frequently, or the UX is highly interactive (rich editors, collaborative tools).
Strategy
Rendered At
Fresh Data
Per-User
SEO
Performance
SSG
Build time
❌
❌
✅
⚡⚡⚡
ISR
Build + interval
◑
❌
✅
⚡⚡⚡
SSR
Request time
✅
✅
✅
⚡⚡
CSR
Browser
✅
✅
❌
⚡
The SEO Advantage
Search engine crawlers have improved significantly in their ability to execute JavaScript, but SSR still offers a decisive SEO advantage in two ways.
First, completeness. Googlebot may execute JavaScript, but it does so asynchronously and with lower priority than HTML. A page that arrives pre-rendered is indexed faster and more reliably than one that requires JavaScript execution.
Second, metadata. Dynamic <title>, <meta description>, and Open Graph tags must be present in the initial HTML to be picked up correctly by crawlers and social media link previewers. SSR ensures this.
In the App Router, metadata is declarative and server-rendered by default:
This metadata is injected into the <head> on the server before any response reaches the browser.
Caching and Performance Considerations
SSR has a reputation for being slower than SSG, and in the naive case, that's true — you're doing work on every request instead of once at build time. But Next.js provides several tools to close this gap.
Route-level caching (App Router)
In the App Router, fetch calls are automatically memoized and deduped within a single request. Multiple server components fetching the same resource will share a single network call.
// These two components can call the same endpoint independently
// Next.js will make only ONE fetch request per render
// ProductHeader.tsx
const product = await fetch(`/api/products/${id}`).then(r => r.json())
// ProductReviews.tsx
const product = await fetch(`/api/products/${id}`).then(r => r.json())
Data Cache
Next.js extends the native fetch API with a persistent data cache:
// Cache this response for 1 hour, even across requests
const data = await fetch('https://api.example.com/config', {
next: { revalidate: 3600 }
})
// Opt out of caching entirely (truly dynamic)
const liveData = await fetch('https://api.example.com/ticker', {
cache: 'no-store'
})
Full Route Cache
Static segments of your application can still be cached at the edge, even within an otherwise dynamic app. A layout component with no dynamic data will be cached and served from the CDN; only the leaf page that needs user-specific data will be rendered server-side on each request.
Error Handling in SSR
Server-side errors need to be handled gracefully. In the App Router, error.tsx files create error boundaries at any level of the route hierarchy:
// app/dashboard/error.tsx
'use client' // Error boundaries must be client components
export default function DashboardError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div>
<h2>Something went wrong loading your dashboard.</h2>
<p>{error.message}</p>
<button onClick={reset}>Try again</button>
</div>
)
}
For loading states, loading.tsx creates a Suspense boundary that displays instantly while the server is still fetching data:
This combination — page.tsx for the real content, loading.tsx for the skeleton, error.tsx for failures — gives you a declarative, file-based way to handle all states of a server-rendered page.
When Not to Use SSR
SSR is powerful, but it's not the right tool for every situation. Reach for a different strategy when:
Content is purely static: A blog post that changes monthly doesn't need to hit your database on every request. Use SSG or ISR.
Latency is critical: SSR adds server processing time to every request. For globally distributed audiences, a statically generated page served from a CDN edge node will always outperform a server-rendered page from a single origin.
The page is fully private and SEO is irrelevant: A complex data visualization tool behind a login wall might be better served with CSR, avoiding the complexity of server rendering.
Your server can't handle the load: SSR puts compute burden on your servers proportional to traffic. Under heavy load without proper caching, this can become expensive. Plan accordingly.
A Practical Decision Framework
When starting a new page or route in Next.js, ask these questions in order:
Is the content the same for every user?
├── Yes → Can it be pre-built?
│ ├── Yes, and it rarely changes → SSG
│ └── Yes, but it changes periodically → ISR
└── No → Does it need real-time data or auth context?
├── Yes → SSR (Server Component or getServerSideProps)
└── No, just user preference/state → CSR with useState
In practice, most production Next.js applications use all four strategies simultaneously — different routes serve different rendering needs, and the framework handles the complexity of mixing them.
Conclusion
Server-Side Rendering in Next.js is not a monolithic feature — it's a spectrum of tools for solving a fundamental problem: how do you deliver the right content, to the right user, as fast as possible?
The Pages Router made SSR explicit and straightforward via getServerSideProps. The App Router with React Server Components pushes the model further: the entire component tree defaults to server-rendered, and you selectively opt into client-side interactivity only where you need it.
What both approaches share is the conviction that the server is not an obstacle between your data and your user — it's an asset. Used well, it means your users see real, meaningful content the moment their request resolves, your search rankings reflect the full richness of your pages, and your architecture draws a clean line between "what the server knows" and "what the user controls."
That clarity, in a framework as flexible as Next.js, is worth a great deal.