DEV Community

Mittal Technologies
Mittal Technologies

Posted on

Server-Side Rendering vs Client-Side Rendering: What Nobody Tells You Until It's Too Late


Every few months someone in Discord or Slack I'm in asks the SSR vs CSR question, and the answers are usually the same: "SSR is better for SEO," "CSR is better for interactivity," "it depends." All true. None of it particularly useful when you're staring at a greenfield project or trying to figure out why your React app tanks on Lighthouse.

So let me try to be actually specific about this.

The mental model that makes this click

Here's the thing that I wish someone had explained clearly when I was first navigating this: SSR and CSR aren't really competing architectural philosophies; they're answers to a specific question: at what point in the journey from server to browser does your HTML get built?

Server-Side Rendering (SSR): The HTML is fully constructed on the server before it reaches the browser. The user gets a complete, readable page on first load. JavaScript may hydrate it afterward for interactivity, but the content is already there.

Client-Side Rendering (CSR):
The server sends a mostly empty HTML shell. The browser downloads JavaScript, executes it, makes API calls, and then builds the DOM. The page content arrives after several roundtrips.

That distinction has real consequences depending on what you're building.

Where CSR actually makes sense (and developers keep picking it for the wrong reasons)

CSR became the default for a lot of teams for one reason: React, Vue, and Angular all default to it, and those are the frameworks people know. So, there's a strong pull toward CSR just from familiarity and tooling momentum.

And for certain apps, CSR is genuinely the right choice. Highly interactive dashboards, admin panels, SaaS tools where SEO doesn't matter and users are authenticated anyway, CSR is fine there. You don't need SSR to power an internal CRM or a data visualization tool behind a login.

The problem is when CSR gets applied to content-heavy, public-facing sites. Here's what that actually looks like in the network tab:

GET / → 200 OK (returns ~1KB HTML shell)
GET /bundle.js → 200 OK (returns 800KB+ JS bundle)
[JS executes]
GET /api/posts → 200 OK (returns actual content)
[DOM renders content]

The user sees a blank or loading screen for the entire duration of that chain. On a fast connection, it's maybe 1-2 seconds. On a 3G connection or a low-end Android device, the kind that a significant chunk of global users actually have, it can be 6-10 seconds of nothing. And Google's crawler, even though it can execute JavaScript, still has to wait for that execution before it can index your content.

That's not a theoretical problem. That's why so many React SPAs have poor Lighthouse scores and mediocre search visibility despite genuinely good content.

SSR done right and the hydration problem people don't warn you about

SSR solves the above by moving HTML construction to the server. Here's the same request flow:

GET / → 200 OK (returns ~50KB complete HTML)
[Browser renders immediately — user sees content]
GET /bundle.js → 200 OK (hydrates for interactivity)

The content is visible almost immediately. LCP scores are dramatically better. Search engines get complete HTML to index on the first fetch.

In Next.js, the simplest SSR pattern looks like this:

// pages/posts/[slug].js
export async function getServerSideProps(context) {
  const { slug } = context.params;
  const post = await fetchPostBySlug(slug); // runs on server
  return {
    props: { post },
  };
}
export default function PostPage({ post }) {
  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}
Enter fullscreen mode Exit fullscreen mode

The HTML returned to the browser already contains the post content. The browser doesn't need to wait for JavaScript execution to see anything meaningful.

But here's what nobody adequately warns you about: hydration mismatches. When React "takes over" the server-rendered HTML and attaches event listeners, it needs the client-side render tree to match what the server sent. If they diverge, because of timestamps, random IDs, localStorage reads during render, browser-only APIs, React throws warnings or silently re-renders, potentially causing layout shift or interactivity bugs.

// This breaks SSR — window doesn't exist on server
const theme = window.localStorage.getItem('theme');

// Safe pattern — check environment first
const theme = typeof window !== 'undefined'
  ? window.localStorage.getItem('theme')
  : 'light';

Enter fullscreen mode Exit fullscreen mode

This is a class of bug that only surfaces in SSR environments. If you've only ever built CSR apps, it'll catch you.

Static Site Generation: the option that beats both for the right use case

Worth mentioning because it often gets collapsed into the SSR conversation: SSG (Static Site Generation) builds HTML at build time rather than request time. For content that doesn't change per-request, blog posts, marketing pages, documentation, this is almost always the right call.

// Next.js SSG
export async function getStaticProps() {
  const posts = await fetchAllPosts();
  return {
    props: { posts },
    revalidate: 3600, // ISR: regenerate every hour
  };
}
Enter fullscreen mode Exit fullscreen mode

The HTML is pre-built, served from a CDN edge, and requires zero server computation per request. Time to the first byte is measured in single-digit milliseconds. This is how you get a 99 Lighthouse performance score without heroic optimization effort.

The limitation is obviously dynamic content, if your page needs user-specific data or changes frequently, pure SSG doesn't work. Incremental Static Regeneration (ISR) in Next.js handles a lot of the middle ground.

The Practical Decision Framework

Rather than "SSR vs CSR," I now think about it as:
Use SSG/ISR for: public content pages, marketing, blogs, documentation, ecommerce product listings. Content doesn't change per-request; SEO matters; performance is critical.
Use SSR for: pages requiring real-time server data, user-specific server-rendered content (dashboards with server session data), or content that must be fresh on every request.
Use CSR for: authenticated application interfaces, heavily interactive tools, anything where SEO is irrelevant and you need maximum client-side flexibility.
Use a hybrid (which Next.js App Router, Nuxt 3, and SvelteKit all support natively): SSG/SSR for the outer shell and public content, CSR components for the interactive pieces.

The businesses and products I've seen handle web performance well, including teams I've worked alongside at Mittal Technologies building web applications for clients, tend to think at this level of granularity. Not "we're an SSR shop" or "we build SPAs," but "this page/route needs this rendering strategy for these specific reasons."

One last thing: measure before you decide

If you're revisiting an existing app's rendering strategy, run Chrome DevTools Coverage on your JavaScript bundle. It's not unusual to find that 40-60% of the JS being shipped to every page visitor is code that runs on maybe 10% of user interactions.

That's not a rendering strategy problem; that's a code splitting problem. And solving it might deliver more Lighthouse improvement than switching rendering strategies entirely.

Analyse your bundle before assuming you need SSR
npx @ next/bundle-analyzer
or for Vite/Vue
npx vite-bundle-visualizer

Understand what you're shipping before you redesign how you're shipping it.

Top comments (0)