React 88 ⚛️ Server-Side Rendering (SSR) vs Static Site Generation (SSG)
The difference between SSR and SSG is not about the code you write. It is about when the HTML is generated. In both cases, React components render on a server, the output is HTML, and the browser receives a page that is already populated with content before any JavaScript runs. The distinction is the trigger: SSG generates the HTML at build time, once, and serves the same file to every visitor. SSR generates the HTML on every request, with access to request-specific data like cookies, headers, and the current user’s session . The same React component can be used in both modes; the framework decides which strategy applies based on the data dependencies and the route configuration.
The choice between them is a choice about freshness and cost. SSG produces the fastest possible response because the HTML is a static file served from a CDN edge, with no server compute in the request path. SSR produces the freshest possible response because the server renders the page with the data that exists at the moment of the request, but every request pays the cost of rendering and data fetching . The modern framework answer is that you do not have to choose one for the whole application; you choose per route, and sometimes per component, based on how that content behaves .
Key point: SSG generates HTML at build time and serves the same static file to every visitor. SSR generates HTML on every request and can use request-specific data. SSG is faster and cheaper; SSR is fresher and more personalized. Both produce fully populated HTML for the first response, which is what makes them different from client-side rendering.
Why SSR and SSG exist
The client-side rendering problem. A pure client-side React application sends an empty HTML shell to the browser. The browser downloads the JavaScript bundle, executes it, mounts the React tree, fetches data, and only then renders the content. The user sees a blank screen or a loading spinner during that entire chain, and a crawler that does not execute JavaScript sees nothing at all . SSR and SSG exist to put the content in the first HTTP response, so the user sees the page immediately and the crawler sees the content without running any JavaScript .
The data-timing problem. The core distinction between the two strategies is when the data is fetched. In SSG, the data is fetched at build time, and the HTML is generated with that data baked in. The data is as fresh as the last build. In SSR, the data is fetched on every request, and the HTML is generated with the data from that moment. The data is as fresh as the request . The framework’s data-fetching functions encode this distinction: getStaticProps runs at build time, and getServerSideProps runs on every request .
The hydration problem. Both strategies produce HTML, but the HTML is not interactive. React needs to “hydrate” the static markup by attaching event listeners and reconciling the virtual DOM with the server-generated HTML. This hydration step requires the JavaScript bundle to download and execute, which means the page is visible before it is interactive. The “time to interactive” is later than the “first contentful paint,” and the gap is the hydration cost . The server-rendered HTML improves the perceived load time, but it does not eliminate the JavaScript payload.
The infrastructure problem. SSG produces static files, and static files can be served from a CDN with no server process. The hosting cost is low and the scaling is trivial. SSR requires a persistent Node.js process that can render the page on demand, and that process needs compute, memory, and a deployment pipeline that can scale with traffic . The infrastructure cost is the practical reason many applications choose SSG for the pages that do not need per-request freshness.
a. SSG: the build-time strategy
SSG renders the page during the build, before any user visits it. The framework runs the data-fetching function, renders the React tree to HTML, and writes the HTML file to the build output. In production, the server or CDN serves that file directly, with no server-side rendering in the request path .
In Next.js Pages Router, the data-fetching function is getStaticProps. It runs at build time, fetches the data, and passes it to the page component as props. The page is then rendered to HTML with the data already present .
// pages/posts/[id].js
// getStaticPaths decides which dynamic routes to build
export async function getStaticPaths() {
// Fetch the list of posts at build time
const res = await fetch('https://api.example.com/posts')
const posts = await res.json()
// Map each post to a path object
const paths = posts.map((post) => ({
params: { id: post.id.toString() },
}))
// fallback: false means any path not returned here is a 404
return { paths, fallback: false }
}
// getStaticProps fetches the data for a single post at build time
export async function getStaticProps({ params }) {
// Fetch the post by its id
const res = await fetch(`https://api.example.com/posts/${params.id}`)
const post = await res.json()
// Return the data as props to the page component
return { props: { post } }
}
// The page component receives the data as props
export default function Post({ post }) {
return <article>{post.title}</article>
}
The getStaticPaths function is the mechanism for dynamic routes. It returns the list of paths that should be generated at build time. The fallback: false setting means that any request for a path not in the list returns a 404 . The fallback: true or fallback: 'blocking' settings generate the page on first request and cache it for subsequent requests, which is a hybrid between SSG and SSR called Incremental Static Regeneration .
The strength of SSG is the response time. The HTML is a static file, and a CDN can serve it from an edge location close to the user. There is no server compute, no database query, and no rendering cost in the request path. A study comparing Next.js rendering strategies found that SSG completed page rendering faster than both SSR and CSR in the tested scenarios . The weakness is staleness: the page reflects the data that existed at build time, and any change requires a new build or a revalidation mechanism.
b. SSR: the request-time strategy
SSR renders the page on every request. The framework runs the data-fetching function with access to the request object, renders the React tree to HTML, and sends the HTML to the browser. The server process must be running for every request, and the rendering cost is paid on each one .
In Next.js Pages Router, the data-fetching function is getServerSideProps. It runs on every request, receives the request context, and returns the data as props .
// pages/profile.js
// getServerSideProps runs on every request
export async function getServerSideProps(context) {
// The context contains the request, response, and query
const { req } = context
// Read the session cookie from the request
const session = req.cookies.session
// If there is no session, redirect to the login page
if (!session) {
return {
redirect: {
destination: '/login',
permanent: false,
},
}
}
// Fetch the user data using the session
const res = await fetch('https://api.example.com/me', {
headers: { Authorization: `Bearer ${session}` },
})
const user = await res.json()
// Return the user data as props
return { props: { user } }
}
// The page component receives the request-specific data
export default function Profile({ user }) {
return <h1>Welcome, {user.name}</h1>
}
The context object is what makes SSR different from SSG. It contains the req and res objects, the query parameters, and the resolved URL. The data-fetching function can read cookies, check headers, and return a redirect or a 404 before the page renders . This is the mechanism for per-request personalization and authorization.
The cost of SSR is the per-request rendering. Every request triggers the data fetch, the React render, and the HTML serialization. Under load, the server needs enough compute to handle the concurrent renders, and caching strategies are necessary to keep the response time reasonable . The performance study found that SSR completed rendering in some scenarios but was slower than SSG in others, and the server load was higher because every request is fully processed .
c. The hybrid reality and the decision framework
Modern frameworks do not force a single strategy for the whole application. Next.js App Router, Remix, and SvelteKit all support per-route decisions. A page that shows a product catalog can be SSG, a page that shows the user’s cart can be SSR, and a page that shows a live activity feed can use streaming SSR with Suspense boundaries . The framework chooses the strategy based on the data-fetching function and the route configuration, and the developer can mix them in the same application.
// The same application can have both
// pages/about.js uses getStaticProps (SSG)
// pages/dashboard.js uses getServerSideProps (SSR)
// The framework handles the routing and the rendering mode per page
The decision framework is determined by three questions. First, does the content change per request? If yes, SSR. If no, SSG. Second, does the content depend on the user’s session, cookies, or location? If yes, SSR. If no, SSG. Third, is the content updated frequently enough that a rebuild is impractical? If yes, SSR or Incremental Static Regeneration. If no, SSG . The performance study’s conclusion aligns with this: SSG is more cost-efficient because the application is rendered only at build time, and SSR is the right choice when content is user-specific or needs frequent updates .
The hybrid pattern also includes Incremental Static Regeneration, which is SSG with a revalidation interval. The page is generated at build time, served as a static file, and regenerated in the background after a specified time or on demand. It is the middle ground between the freshness of SSR and the speed of SSG .
Complete Example Session
The following session builds an application with one SSG page and one SSR page, showing the data-fetching function and the resulting HTML for each.
// ============================================
// PART 1: THE SSG PAGE
// ============================================
// pages/posts/[id].js
export async function getStaticPaths() {
// Fetch the list of posts at build time
const res = await fetch('https://api.example.com/posts')
const posts = await res.json()
// Generate a path for each post
const paths = posts.map((p) => ({ params: { id: p.id.toString() } }))
// Unknown paths return 404
return { paths, fallback: false }
}
export async function getStaticProps({ params }) {
// Fetch the post data at build time
const res = await fetch(`https://api.example.com/posts/${params.id}`)
const post = await res.json()
// Pass the data to the component
return { props: { post } }
}
export default function Post({ post }) {
// The HTML is generated with the post data already present
return <article>{post.title}</article>
}
// ============================================
// PART 2: THE SSR PAGE
// ============================================
// pages/profile.js
export async function getServerSideProps(context) {
// Read the session cookie from the request
const session = context.req.cookies.session
// Redirect if there is no session
if (!session) {
return { redirect: { destination: '/login', permanent: false } }
}
// Fetch the user data on every request
const res = await fetch('https://api.example.com/me', {
headers: { Authorization: `Bearer ${session}` },
})
const user = await res.json()
// Pass the request-specific data to the component
return { props: { user } }
}
export default function Profile({ user }) {
// The HTML is generated with the current user's data
return <h1>Welcome, {user.name}</h1>
}
The two parts cover the SSG page with getStaticPaths and getStaticProps, and the SSR page with getServerSideProps. What the session demonstrates is that the strategy is determined by the data-fetching function, not by a global application setting.
Quick Reference
SSR vs SSG
| Aspect | SSG | SSR |
|---|---|---|
| HTML generated | Build time | Request time |
| Data freshness | Build-time snapshot | Current request |
| Server process | Not required in request path | Required |
| Response speed | CDN-fast | Server-render cost |
| Personalization | No | Yes (cookies, headers) |
| Hosting | Static / CDN | Node.js server |
When to Use
| Content | Strategy |
|---|---|
| Blog post, docs, marketing | SSG |
| User dashboard, cart | SSR |
| Product catalog (rare changes) | SSG |
| Live inventory, personalized pricing | SSR |
| Frequently updated, not per-user | ISR |
Next.js Data Functions
| Function | Runs | Mode |
|---|---|---|
getStaticProps | Build time | SSG |
getStaticPaths | Build time | SSG (dynamic routes) |
getServerSideProps | Every request | SSR |
Core React APIs
| API | Purpose |
|---|---|
prerender / prerenderToNodeStream | Static generation |
renderToPipeableStream | Streaming SSR |
hydrateRoot | Make HTML interactive |
Best Practices
✅ Do This:
// SSG for static content
export async function getStaticProps() { ... }
// SSR for per-request data
export async function getServerSideProps(context) { ... }
// Use ISR for frequently updated static content
return { props, revalidate: 60 }
❌ Don’t Do This:
// Use SSR for a page that never changes
export async function getServerSideProps() { ... } // wasteful
// Use SSG for a page that needs the session
export async function getStaticProps() { ... } // no request access
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Stale content on SSG page | Build-time data not refreshed | Use ISR or SSR |
| Session unavailable in SSG | No request context | Use SSR |
| Slow response under load | SSR renders every request | Add caching |
| Hydration mismatch | Server and client render different output | Keep data deterministic |
| Build time too long | Too many static paths | Use fallback or SSR |
Real-World Examples
1. SSG blog post
export async function getStaticProps({ params }) { ... }
2. SSR user profile
export async function getServerSideProps(context) { ... }
3. SSG with dynamic paths
export async function getStaticPaths() { ... }
4. ISR revalidation
return { props, revalidate: 60 }
5. SSR redirect
return { redirect: { destination: '/login', permanent: false } }
6. Static generation without data
export default function About() { return <div>About</div> }
7. SSR cookie read
const session = context.req.cookies.session
8. Fallback blocking
return { paths, fallback: 'blocking' }
9. Streaming SSR
renderToPipeableStream(<App />, { bootstrapScripts: ['/main.js'] })
10. Hydration
hydrateRoot(document.getElementById('root'), <App />)
Visual
SSG Build-Time Flow
┌──────────────────────────────────────────────┐
│ Build time │
│ getStaticProps → fetch data │
│ React renders to HTML │
│ HTML written to disk │
│ │ │
│ ▼ │
│ User request → CDN serves static HTML │
│ → browser parses and renders │
│ → hydration makes it interactive │
└──────────────────────────────────────────────┘
SSR Request-Time Flow
┌──────────────────────────────────────────────┐
│ User request → server receives request │
│ getServerSideProps → fetch data │
│ React renders to HTML │
│ HTML sent to browser │
│ → browser parses and renders │
│ → hydration makes it interactive │
└──────────────────────────────────────────────┘
The Hydration Gap
┌──────────────────────────────────────────────┐
│ HTML arrives → First Contentful Paint │
│ JavaScript downloads → executes │
│ Hydration completes → Time to Interactive │
│ │
│ The page is visible before it is usable. │
└──────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| SSG | HTML at build time |
| SSR | HTML at request time |
| SSG strength | Speed, CDN, low cost |
| SSR strength | Freshness, personalization |
| SSG weakness | Stale content |
| SSR weakness | Server cost, load |
| Hydration | Required for both |
| ISR | SSG with revalidation |
| Next.js SSG | getStaticProps |
| Next.js SSR | getServerSideProps |
Key takeaways:
- The difference is when, not what. Both strategies render React on a server and send populated HTML to the browser. SSG does it at build time; SSR does it on every request .
- SSG is faster and cheaper. The HTML is a static file served from a CDN, with no server compute in the request path. SSR pays the rendering cost on every request .
- SSR is fresher and more personalized. It has access to the request object, so it can read cookies, headers, and session data. SSG cannot .
- Hydration is required for both. The server-rendered HTML is not interactive until the JavaScript bundle downloads and executes. The gap between first paint and interactivity is the hydration cost .
- The decision is per route. Modern frameworks let you choose SSG for static pages and SSR for dynamic pages in the same application .
- ISR is the middle ground. It generates the page at build time and revalidates it in the background, combining SSG’s speed with a bounded staleness .
Remember: SSR and SSG are not competing philosophies. They are two answers to the same question—when should the HTML be generated—and the correct answer depends on the page. A blog post that changes once a week should be SSG. A dashboard that shows the current user’s data should be SSR. A product listing that changes hourly can use ISR. The framework provides the functions; the developer chooses the timing. The performance and cost consequences follow from that choice, and the hydration cost is paid either way.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!