| |

React 88 ⚛️ Server-Side Rendering (SSR) vs Static Site Generation (SSG)

⏱️ Estimated read: 13 min | |

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

AspectSSGSSR
HTML generatedBuild timeRequest time
Data freshnessBuild-time snapshotCurrent request
Server processNot required in request pathRequired
Response speedCDN-fastServer-render cost
PersonalizationNoYes (cookies, headers)
HostingStatic / CDNNode.js server

When to Use

ContentStrategy
Blog post, docs, marketingSSG
User dashboard, cartSSR
Product catalog (rare changes)SSG
Live inventory, personalized pricingSSR
Frequently updated, not per-userISR

Next.js Data Functions

FunctionRunsMode
getStaticPropsBuild timeSSG
getStaticPathsBuild timeSSG (dynamic routes)
getServerSidePropsEvery requestSSR

Core React APIs

APIPurpose
prerender / prerenderToNodeStreamStatic generation
renderToPipeableStreamStreaming SSR
hydrateRootMake 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

PitfallWhy It HappensFix
Stale content on SSG pageBuild-time data not refreshedUse ISR or SSR
Session unavailable in SSGNo request contextUse SSR
Slow response under loadSSR renders every requestAdd caching
Hydration mismatchServer and client render different outputKeep data deterministic
Build time too longToo many static pathsUse 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

ItemValue
SSGHTML at build time
SSRHTML at request time
SSG strengthSpeed, CDN, low cost
SSR strengthFreshness, personalization
SSG weaknessStale content
SSR weaknessServer cost, load
HydrationRequired for both
ISRSSG with revalidation
Next.js SSGgetStaticProps
Next.js SSRgetServerSideProps

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.


Was this helpful?


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!