Robin.te.Hofstee

Server-Side Rendering vs Client-Side Rendering Explained

Robin te Hofstee16 min readViews0
Server-Side Rendering vs Client-Side Rendering Explained

Server-Side Rendering vs Client-Side Rendering Explained

Introduction

One of the most important architectural decisions in modern web development is choosing between Server-Side Rendering (SSR) and Client-Side Rendering (CSR). This choice significantly impacts your application's performance, SEO, user experience, and development workflow.

In this comprehensive guide, we'll explore both approaches, understand their strengths and weaknesses, and learn when to use each one.

What is Client-Side Rendering (CSR)?

Client-Side Rendering means the browser downloads a minimal HTML page, downloads JavaScript, and then JavaScript renders the content in the browser.

How CSR Works

1. Browser requests page
2. Server sends minimal HTML + JavaScript bundle
3. Browser downloads JavaScript
4. JavaScript executes and fetches data
5. React renders UI in the browser
6. Page becomes interactive

Example: Traditional React App (CSR)

<!-- index.html - What the server sends -->
<!DOCTYPE html>
<html>
  <head>
    <title>My App</title>
  </head>
  <body>
    <div id="root"></div>
    <script src="/bundle.js"></script>
  </body>
</html>
// App.jsx - JavaScript renders everything
import { useState, useEffect } from 'react';
 
function App() {
  const [posts, setPosts] = useState([]);
  const [loading, setLoading] = useState(true);
 
  useEffect(() => {
    // Data fetched in the browser
    fetch('/api/posts')
      .then(res => res.json())
      .then(data => {
        setPosts(data);
        setLoading(false);
      });
  }, []);
 
  if (loading) return <div>Loading...</div>;
 
  return (
    <div>
      <h1>Blog Posts</h1>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.content}</p>
        </article>
      ))}
    </div>
  );
}
 
export default App;

Timeline:

0ms   : Browser receives HTML (nearly empty)
100ms : JavaScript downloaded
200ms : JavaScript parsed and executed
300ms : API call starts
500ms : Data received
550ms : UI rendered
600ms : Page interactive

What is Server-Side Rendering (SSR)?

Server-Side Rendering means the server generates the full HTML content with data, sends it to the browser, and then JavaScript "hydrates" to make it interactive.

How SSR Works

1. Browser requests page
2. Server fetches data
3. Server renders React to HTML
4. Server sends complete HTML
5. Browser displays content immediately
6. Browser downloads JavaScript
7. React "hydrates" (attaches event handlers)
8. Page becomes interactive

Example: Next.js with SSR

// pages/posts.jsx - Next.js SSR
export async function getServerSideProps() {
  // Data fetched on the server
  const res = await fetch('https://api.example.com/posts');
  const posts = await res.json();
 
  return {
    props: { posts }, // Passed to component
  };
}
 
export default function Posts({ posts }) {
  // Component receives data as props
  return (
    <div>
      <h1>Blog Posts</h1>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.content}</p>
        </article>
      ))}
    </div>
  );
}

What the browser receives (full HTML):

<!DOCTYPE html>
<html>
  <head>
    <title>Blog Posts</title>
  </head>
  <body>
    <div id="__next">
      <div>
        <h1>Blog Posts</h1>
        <article>
          <h2>First Post</h2>
          <p>This is the content...</p>
        </article>
        <!-- More posts... -->
      </div>
    </div>
    <script src="/_next/static/chunks/main.js"></script>
  </body>
</html>

Timeline:

0ms   : Server starts rendering
200ms : Server fetches data
400ms : Server renders HTML
450ms : Browser receives complete HTML
450ms : Content visible (First Contentful Paint)
550ms : JavaScript downloaded
650ms : Hydration complete
700ms : Page interactive (Time to Interactive)

Side-by-Side Comparison

AspectCSRSSR
Initial LoadSlower (blank page, then content)Faster (content immediately visible)
Time to InteractiveSlowerFaster
SEOPoor (crawlers see empty HTML)Excellent (crawlers see full content)
Server LoadLowHigh
CachingEasy (static files)Complex (dynamic content)
NavigationInstant (no page reload)May require full page load
Data FreshnessAlways freshCan be stale
Hosting CostCheap (static hosting)Expensive (needs server)
DevelopmentSimpleMore complex

Deep Dive: Performance Metrics

CSR Performance

// CSR Timeline Example
import { useState, useEffect } from 'react';
 
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
 
  useEffect(() => {
    // Fetch happens AFTER component mounts
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => {
        setUser(data);
        setLoading(false);
      });
  }, [userId]);
 
  if (loading) {
    return <Spinner />; // User sees loading state
  }
 
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.bio}</p>
    </div>
  );
}

User Experience:

  1. 0ms: Blank screen
  2. 200ms: Loading spinner appears
  3. 700ms: Content appears
  4. Total wait: 700ms

SSR Performance

// SSR Timeline Example (Next.js)
export async function getServerSideProps({ params }) {
  // Fetch happens on SERVER before rendering
  const res = await fetch(`https://api.example.com/users/${params.userId}`);
  const user = await res.json();
 
  return { props: { user } };
}
 
export default function UserProfile({ user }) {
  // Data is already here!
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.bio}</p>
    </div>
  );
}

User Experience:

  1. 0ms: Server fetches data
  2. 200ms: Server renders HTML
  3. 250ms: Browser receives complete HTML
  4. 250ms: Content visible
  5. Total wait: 250ms

SEO Comparison

CSR: What Google Sees

<!-- Initial HTML (poor for SEO) -->
<!DOCTYPE html>
<html>
  <head>
    <title>My Blog</title>
  </head>
  <body>
    <div id="root"></div>
    <!-- No content for crawlers! -->
  </body>
</html>

Problems:

  • ❌ Search engines may not wait for JavaScript
  • ❌ No content in initial HTML
  • ❌ Meta tags might not be present
  • ❌ Social media previews won't work

SSR: What Google Sees

<!-- Complete HTML (excellent for SEO) -->
<!DOCTYPE html>
<html>
  <head>
    <title>How to Build Custom Hooks - My Blog</title>
    <meta name="description" content="Learn to build reusable..." />
    <meta property="og:title" content="How to Build Custom Hooks" />
    <meta property="og:image" content="/images/hooks-tutorial.jpg" />
  </head>
  <body>
    <article>
      <h1>How to Build Custom Hooks</h1>
      <p>Custom hooks are a powerful feature...</p>
      <!-- Full content here -->
    </article>
  </body>
</html>

Benefits:

  • ✅ Full content immediately available
  • ✅ Proper meta tags for social sharing
  • ✅ Better search rankings
  • ✅ Rich previews on Twitter, Facebook, etc.

When to Use CSR

✅ Best Use Cases for Client-Side Rendering

1. Internal Dashboards & Admin Panels

// Dashboard doesn't need SEO
function AdminDashboard() {
  const [stats, setStats] = useState(null);
 
  useEffect(() => {
    // User is already logged in, SEO doesn't matter
    fetchAnalytics().then(setStats);
  }, []);
 
  return <AnalyticsCharts data={stats} />;
}

Why CSR: Authentication required, no SEO needs, always fresh data

2. Real-Time Applications

// Chat app with constant updates
function ChatApp() {
  const [messages, setMessages] = useState([]);
 
  useEffect(() => {
    const socket = io();
    socket.on('message', (msg) => {
      setMessages(prev => [...prev, msg]);
    });
  }, []);
 
  return <MessageList messages={messages} />;
}

Why CSR: Frequent updates, WebSocket connections, no SEO needed

3. Single Page Applications (SPAs)

  • Gmail-like email clients
  • Trello-like project management
  • Figma-like design tools
  • Authenticated web apps

4. Interactive Tools & Calculators

// Mortgage calculator
function MortgageCalculator() {
  const [principal, setPrincipal] = useState(200000);
  const [rate, setRate] = useState(3.5);
  const [years, setYears] = useState(30);
 
  const monthlyPayment = calculateMortgage(principal, rate, years);
 
  return (
    <div>
      <input value={principal} onChange={e => setPrincipal(e.target.value)} />
      <input value={rate} onChange={e => setRate(e.target.value)} />
      <input value={years} onChange={e => setYears(e.target.value)} />
      <h2>Monthly Payment: ${monthlyPayment}</h2>
    </div>
  );
}

Why CSR: No external data, instant interactivity, no SEO benefit from server rendering


When to Use SSR

✅ Best Use Cases for Server-Side Rendering

1. Content-Heavy Websites (Blogs, News)

// Blog post page
export async function getServerSideProps({ params }) {
  const post = await fetchPost(params.slug);
  
  return {
    props: {
      post,
      // SEO meta data
      meta: {
        title: post.title,
        description: post.excerpt,
        image: post.coverImage,
      }
    }
  };
}
 
export default function BlogPost({ post, meta }) {
  return (
    <>
      <Head>
        <title>{meta.title}</title>
        <meta name="description" content={meta.description} />
        <meta property="og:image" content={meta.image} />
      </Head>
      <article>
        <h1>{post.title}</h1>
        <div dangerouslySetInnerHTML={{ __html: post.content }} />
      </article>
    </>
  );
}

Why SSR: SEO critical, content is the product, social sharing important

2. E-Commerce Product Pages

// Product detail page
export async function getServerSideProps({ params }) {
  const product = await fetchProduct(params.id);
  
  return {
    props: { product }
  };
}
 
export default function Product({ product }) {
  return (
    <div>
      <h1>{product.name}</h1>
      <img src={product.image} alt={product.name} />
      <p>{product.description}</p>
      <span>${product.price}</span>
      <button>Add to Cart</button>
    </div>
  );
}

Why SSR: SEO for product discovery, social sharing, faster perceived load

3. Landing Pages & Marketing Sites

  • Homepage
  • Pricing pages
  • Feature pages
  • About pages

Why SSR: First impression matters, SEO crucial, conversion optimization

4. Social Media & User Profiles

// Public user profile
export async function getServerSideProps({ params }) {
  const user = await fetchUserProfile(params.username);
  
  return {
    props: {
      user,
      meta: {
        title: `${user.name} (@${user.username})`,
        description: user.bio,
        image: user.avatar,
      }
    }
  };
}

Why SSR: Rich social previews, profile discovery, SEO


The Hybrid Approach: Best of Both Worlds

Modern frameworks like Next.js let you mix SSR and CSR in the same app!

Strategy 1: SSR for Initial Load, CSR for Interactions

// Next.js page with hybrid approach
export async function getServerSideProps() {
  // SSR: Fetch critical data on server
  const initialPosts = await fetchPosts({ limit: 10 });
  
  return {
    props: { initialPosts }
  };
}
 
export default function Blog({ initialPosts }) {
  const [posts, setPosts] = useState(initialPosts);
  const [page, setPage] = useState(1);
 
  // CSR: Load more posts client-side
  const loadMore = async () => {
    const newPosts = await fetchPosts({ page: page + 1 });
    setPosts([...posts, ...newPosts]);
    setPage(page + 1);
  };
 
  return (
    <div>
      {/* SSR content */}
      {posts.map(post => <PostCard key={post.id} post={post} />)}
      
      {/* CSR interaction */}
      <button onClick={loadMore}>Load More</button>
    </div>
  );
}

Strategy 2: Static Generation (SSG) for Static Content

// SSG: Pre-render at build time (fastest!)
export async function getStaticProps() {
  const posts = await fetchAllPosts();
  
  return {
    props: { posts },
    revalidate: 3600, // Re-generate every hour
  };
}
 
export default function Blog({ posts }) {
  return (
    <div>
      {posts.map(post => <PostCard key={post.id} post={post} />)}
    </div>
  );
}

When to use SSG:

  • Content changes infrequently
  • Can be pre-rendered at build time
  • Fastest possible performance
  • Examples: Documentation, blogs, marketing pages

Performance Optimization Tips

Optimizing CSR

// 1. Code splitting
const HeavyComponent = lazy(() => import('./HeavyComponent'));
 
function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <HeavyComponent />
    </Suspense>
  );
}
 
// 2. Prefetch data on hover
function ProductCard({ product }) {
  const prefetchProduct = () => {
    // Start loading before click
    queryClient.prefetchQuery(['product', product.id], () =>
      fetchProduct(product.id)
    );
  };
 
  return (
    <Link 
      to={`/products/${product.id}`}
      onMouseEnter={prefetchProduct}
    >
      {product.name}
    </Link>
  );
}
 
// 3. Optimize bundle size
import { debounce } from 'lodash-es'; // Use ES modules
// Instead of: import _ from 'lodash';

Optimizing SSR

// 1. Cache expensive operations
import { cache } from 'react';
 
const getPosts = cache(async () => {
  // This will be cached during the request
  return await db.posts.findMany();
});
 
// 2. Streaming SSR (React 18+)
import { Suspense } from 'react';
 
export default function Page() {
  return (
    <div>
      <Header /> {/* Rendered immediately */}
      <Suspense fallback={<Skeleton />}>
        <SlowComponent /> {/* Streamed later */}
      </Suspense>
    </div>
  );
}
 
// 3. Edge rendering (deploy closer to users)
export const runtime = 'edge';

Common Misconceptions

❌ Myth 1: "SSR is always faster"

Reality: SSR has better perceived performance (content visible sooner), but Time to Interactive can be slower due to hydration.

❌ Myth 2: "CSR apps can't rank in Google"

Reality: Google can execute JavaScript, but SSR is more reliable and performs better in search.

❌ Myth 3: "I have to choose one approach"

Reality: Modern frameworks let you mix approaches per page or even per component.

❌ Myth 4: "SSR requires expensive servers"

Reality: Edge computing and smart caching make SSR affordable. Static generation (SSG) is even cheaper.


Decision Framework

Use CSR When:

  • ✅ App requires authentication
  • ✅ SEO is not important
  • ✅ Frequent real-time updates
  • ✅ Internal tools/dashboards
  • ✅ Budget/complexity constraints

Use SSR When:

  • ✅ SEO is critical
  • ✅ Social sharing important
  • ✅ Content is the product
  • ✅ First impression matters
  • ✅ Public-facing pages

Use SSG (Static) When:

  • ✅ Content rarely changes
  • ✅ Can pre-render everything
  • ✅ Maximum performance needed
  • ✅ Predictable content structure

Real-World Examples

CSR: Spotify Web Player

  • Authenticated app
  • Real-time updates
  • No SEO benefit
  • Rich interactions

SSR: Medium Articles

  • Content discovery crucial
  • SEO drives traffic
  • Social sharing important
  • Fast perceived load

Hybrid: Next.js Commerce

  • Product pages: SSR (SEO)
  • Cart: CSR (interactivity)
  • Homepage: SSG (performance)
  • Search: CSR (real-time)

Migration Strategies

Moving from CSR to SSR

// Before (Create React App)
function Blog() {
  const [posts, setPosts] = useState([]);
  
  useEffect(() => {
    fetch('/api/posts').then(res => res.json()).then(setPosts);
  }, []);
  
  return <PostList posts={posts} />;
}
 
// After (Next.js)
export async function getServerSideProps() {
  const res = await fetch('https://api.example.com/posts');
  const posts = await res.json();
  return { props: { posts } };
}
 
export default function Blog({ posts }) {
  return <PostList posts={posts} />;
}

Migration Steps:

  1. Set up Next.js
  2. Move pages to pages/ directory
  3. Replace useEffect data fetching with getServerSideProps
  4. Update API calls to use absolute URLs
  5. Test and deploy

Tools & Frameworks

CSR Frameworks

  • React (Create React App)
  • Vue.js (Vue CLI)
  • Angular
  • Svelte

SSR Frameworks

  • Next.js (React)
  • Nuxt.js (Vue)
  • SvelteKit (Svelte)
  • Remix (React)
  • Astro (Multi-framework)

Testing Performance

# Lighthouse (Chrome DevTools)
lighthouse https://example.com --view
 
# WebPageTest
# Visit webpagetest.org
 
# Core Web Vitals
# Check in Google Search Console

Conclusion

Both SSR and CSR have their place in modern web development:

Choose SSR when:

  • SEO and discoverability matter
  • Content is your product
  • First impressions are critical

Choose CSR when:

  • Building authenticated apps
  • Real-time updates are core
  • SEO is not a concern

The future is hybrid: Most modern apps benefit from mixing approaches, using SSR for public pages and CSR for interactive features.

Remember: The best choice depends on your specific use case. Measure, test, and optimize based on your actual users' needs!


Further Reading


Questions or feedback? Drop a comment below or reach out on Twitter/X!

Found this helpful? Share it with your fellow developers! 🚀

Share this post
Robin te Hofstee

Written by Robin te Hofstee

Technology enthusiast with a deep love for programming. Sharing practical guides on React, Next.js, Linux and self-hosting.

Next.js App Router Deep Dive

2

A practical deep dive into the Next.js App Router: layouts, dynamic routes, Server and Client Components, data fetching, caching, Server Actions, route handlers, and the pitfalls to avoid.