DEV Community

Cover image for CSR vs SSR in Next.js
Khaled Saeed
Khaled Saeed

Posted on

CSR vs SSR in Next.js

CSR vs SSR in Next.js: From Basics to Advanced Implementation

Introduction

When building modern web applications with Next.js, one of the most crucial decisions you'll make is choosing the right rendering strategy. Client-Side Rendering (CSR) and Server-Side Rendering (SSR) each have their strengths and use cases. This guide will walk you through everything you need to know about CSR vs SSR in Next.js.

What is Rendering?

Rendering is the process of converting your React components into HTML that can be displayed in the browser.

Next.js supports multiple rendering methods:

  • CSR (Client-Side Rendering)
  • SSR (Server-Side Rendering)
  • SSG (Static Site Generation)
  • ISR (Incremental Static Regeneration)

But let’s focus on CSR and SSR.


What is CSR?

Client-Side Rendering (CSR) is a rendering approach where the initial HTML sent to the browser is minimal, and JavaScript running in the browser generates the content dynamically.

How CSR Works:

CSR Sequence Diagram


What is SSR?

Server-Side Rendering (SSR) is a rendering approach where the HTML is generated on the server for each request, then sent to the browser as a fully-formed page.

How SSR Works:

SSR Sequence Diagram


Key Differences

Aspect CSR SSR
Initial Load Slower (JavaScript must load first) Faster (HTML ready immediately)
SEO Poor (content loaded via JS) Excellent (content in HTML)
Server Load Lower Higher
Subsequent Navigation Faster Slower
User Experience Blank page → Content appears Content appears immediately
Caching Easier to cache assets More complex caching

Comparison Timeline


Code Examples

Client-Side Rendering Example

// pages/csr-example.js
import { useState, useEffect } from 'react';

function CSRExample() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // This runs only on the client
    fetch('/api/data')
      .then(response => response.json())
      .then(data => {
        setData(data);
        setLoading(false);
      });
  }, []);

  if (loading) return <div>Loading...</div>;

  return (
    <div>
      <h1>Client-Side Rendered Page</h1>
      <p>Data: {data?.message}</p>
    </div>
  );
}

export default CSRExample;
Enter fullscreen mode Exit fullscreen mode

Server-Side Rendering Example

// pages/ssr-example.js
function SSRExample({ data }) {
  return (
    <div>
      <h1>Server-Side Rendered Page</h1>
      <p>Data: {data.message}</p>
      <p>Generated at: {data.timestamp}</p>
    </div>
  );
}

// This function runs on the server for each request
export async function getServerSideProps() {
  // Fetch data from external API
  const res = await fetch('https://api.example.com/data');
  const data = await res.json();

  return {
    props: {
      data: {
        ...data,
        timestamp: new Date().toISOString()
      }
    }
  };
}

export default SSRExample;
Enter fullscreen mode Exit fullscreen mode

Performance Comparison

Time to First Byte (TTFB)

  • CSR: Fast TTFB (but slower content rendering)
  • SSR: Slower (server processing time)

First Contentful Paint (FCP)

  • CSR: Slow (wait for JS execution)
  • SSR: Fast (HTML ready immediately)

Time to Interactive (TTI)

  • CSR: Slow (dependent on JS bundle size)
  • SSR: Moderate (hydration required)

Performance Metrics Comparison


Performance Optimization Example

// pages/optimized-ssr.js
import dynamic from 'next/dynamic';

// Lazy load heavy components
// ! `ssr: false` turns the component into a client component.
const HeavyComponent = dynamic(() => import('../components/HeavyComponent'), {
  loading: () => <p>Loading...</p>,
  ssr: false // Disable SSR for this component
});

function OptimizedSSR({ criticalData }) {
  return (
    <div>
      <h1>Optimized SSR Page</h1>
      <div>Critical content: {criticalData.message}</div>

      {/* This component loads only on client */}
      <HeavyComponent />
    </div>
  );
}

export async function getServerSideProps() {
  // Only fetch critical data on server
  const res = await fetch('https://api.example.com/critical-data');
  const criticalData = await res.json();

  return {
    props: {
      criticalData
    }
  };
}

export default OptimizedSSR;
Enter fullscreen mode Exit fullscreen mode

When to Use What

Use CSR When:

  • Building a dashboard or admin panel
  • User interaction is frequent
  • SEO is not a priority
  • You need real-time updates
  • Building a Single Page Application (SPA)

Use SSR When:

  • SEO is crucial
  • First-page load performance matters
  • Content changes frequently
  • Building e-commerce sites
  • Social media sharing is important

Advanced Topics

Dynamic Imports for Code Splitting

// Advanced code splitting example
import dynamic from 'next/dynamic';
import { useState } from 'react';

const DynamicChart = dynamic(() => import('../components/Chart'), {
  loading: () => <p>Loading chart...</p>,
  ssr: false
});

const DynamicTable = dynamic(() => import('../components/Table'), {
  loading: () => <p>Loading table...</p>
});

function AdvancedCSR() {
  const [view, setView] = useState('chart');

  return (
    <div>
      <nav>
        <button onClick={() => setView('chart')}>Chart View</button>
        <button onClick={() => setView('table')}>Table View</button>
      </nav>

      {view === 'chart' && <DynamicChart />}
      {view === 'table' && <DynamicTable />}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Streaming SSR (React 18+)

// pages/streaming-ssr.js
import { Suspense } from 'react';

function SlowComponent() {
  // Simulate slow component
  await new Promise((res) => setTimeout(res, 3000));
  return <div>Slow loading content</div>;
}

function StreamingSSR() {
  return (
    <div>
      <h1>Streaming SSR Example</h1>
      <p>This content loads immediately</p>

      <Suspense fallback={<div>Loading slow content...</div>}>
        <SlowComponent />
      </Suspense>
    </div>
  );
}

export default StreamingSSR;
Enter fullscreen mode Exit fullscreen mode

Edge-Side Rendering with Middleware

// middleware.js
import { NextResponse } from 'next/server';

export function middleware(request) {
  const country = request.geo?.country || 'US';

  // Customize response based on location
  const response = NextResponse.next();
  response.headers.set('x-country', country);

  return response;
}

export const config = {
  matcher: '/api/:path*'
};

// Note: Middleware runs before requests but cannot render HTML. it's useful for things like redirects, geolocation, or A/B testing logic.
Enter fullscreen mode Exit fullscreen mode

Best Practices

1. Choose the Right Rendering Strategy

// Decision matrix in code
const getRenderingStrategy = (pageType, requirements) => {
  if (requirements.seo && requirements.freshData) {
    return 'SSR';
  }

  if (requirements.interactivity && !requirements.seo) {
    return 'CSR';
  }

  return 'Hybrid';
};
Enter fullscreen mode Exit fullscreen mode

2. Optimize Bundle Size

// next.config.js
module.exports = {
  experimental: {
    css: true,
  },
  webpack: (config) => {
    config.optimization.splitChunks.chunks = 'all';
    return config;
  }
};
Enter fullscreen mode Exit fullscreen mode

3. Implement Progressive Enhancement

// Progressive enhancement example
function ProgressiveForm() {
  const [isClient, setIsClient] = useState(false);

  useEffect(() => {
    setIsClient(true);
  }, []);

  return (
    <form>
      <input type="text" name="name" required />
      <input type="email" name="email" required />

      {/* Enhanced features only on client */}
      {isClient && (
        <div>
          <AutoComplete />
          <RealTimeValidation />
        </div>
      )}

      <button type="submit">Submit</button>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

4. Error Handling and Loading States

// Comprehensive error handling
function RobustSSR({ data, error }) {
  if (error) {
    return <ErrorComponent error={error} />;
  }

  return (
    <div>
      <h1>Robust SSR Page</h1>
      <p>{data?.message}</p>
    </div>
  );
}

export async function getServerSideProps() {
  try {
    const res = await fetch('https://api.example.com/data');

    if (!res.ok) {
      throw new Error('Failed to fetch data');
    }

    const data = await res.json();

    return {
      props: { data }
    };
  } catch (error) {
    return {
      props: {
        error: error.message
      }
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Choosing between CSR and SSR in Next.js isn't about picking one over the other, it's about understanding your application's requirements and users needs. Here's a quick recap:

  • Use SSR for SEO-critical pages and better initial load performance
  • Use CSR for highly interactive applications where SEO isn't a priority

Next.js makes it easy to implement any of these strategies, and you can even mix them within the same application. The key is to profile your application, understand your users' journey, and choose the rendering strategy that provides the best user experience.

Remember: Performance is not just about fast loading, it's about the right content being available at the right time for your users.

📚 Further Reading: Server and Client Components

Top comments (1)

Collapse
 
md-afsar-mahmud profile image
Md Afsar Mahmud

thanks for shared