SSR vs. Client-Side Rendering (CSR)

Server-Side Rendering (SSR) and Client-Side Rendering (CSR) are two different approaches to rendering web applications.

Client-Side Rendering (CSR)

  • In CSR, the initial HTML sent to the browser is minimal, usually containing an empty <div> where JavaScript (React) will later hydrate the content.
  • The browser downloads JavaScript, executes it, and renders the content dynamically on the client side.
  • This results in slower initial load times but faster subsequent navigation since most interactions happen client-side.
  • When to Use CSR?
    • When SEO is not a major concern (e.g., web applications that require authentication, dashboards, or internal tools).
    • When the user experience requires a highly dynamic and interactive UI.

Server-Side Rendering (SSR)

  • In SSR, the server generates the HTML on request and sends a fully rendered page to the client.
  • JavaScript on the client then “hydrates” this HTML, making it interactive.
  • This leads to faster initial page loads and better SEO since search engines can index fully-rendered content.
  • When to Use SSR?
    • When SEO and faster first-paint are critical (e.g., marketing websites, e-commerce platforms, news sites).
    • When the application needs to be accessible for users with slow connections or low-end devices.

Understanding Hydration

Hydration is the process where React takes over the pre-rendered HTML (generated by SSR) and attaches event listeners to make it interactive.

How Hydration Works: An Example

Let’s take a simple example of a counter with SSR and hydration:

Server-Side (Rendered HTML Sent to Client)

<div id="counter">
  <p>Count: 0</p>
  <button>Increment</button>
  <button>Decrement</button>
</div>

Client-Side (Hydration in React)

import { useState } from "react";
 
function Counter() {
  const [count, setCount] = useState(0);
 
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setCount(count - 1)}>Decrement</button>
    </div>
  );
}
 
export default Counter;

In an SSR setup (e.g., using Next.js), the initial page would be rendered on the server and sent to the client. React on the client then hydrates the page, attaching event listeners to make the buttons interactive. This is done with bundle.js which is downloaded asynchronously when the user requests for the page.

Why Hydration Matters?

  • Ensures that the HTML generated on the server matches the expected React component structure.
  • Allows interactivity without needing to re-fetch and re-render the entire page.
  • Reduces the time needed for the user to interact with the page.

Common Hydration Errors and Fixes

1. Content Mismatch Error

  • Issue: The HTML sent by the server does not match what React expects on the client.

  • Example:

    function TimeComponent() {
      return <p>Current time: {new Date().toLocaleTimeString()}</p>;
    }
    • Since new Date().toLocaleTimeString() changes with each render, the server’s pre-rendered HTML won’t match what React generates on the client.
  • Fix:

    • Avoid rendering dynamic values that change on every render during SSR.
    • Use useEffect to update dynamic values on the client only.
    import { useState, useEffect } from "react";
     
    function TimeComponent() {
      const [time, setTime] = useState(null);
      useEffect(() => {
        setTime(new Date().toLocaleTimeString());
      }, []);
      return <p>Current time: {time || "Loading..."}</p>;
    }

2. Event Listeners Not Attaching

  • Issue: The HTML loads, but buttons or interactive elements don’t work.
  • Cause: Hydration hasn’t completed yet or JavaScript hasn’t fully executed.
  • Fix:
    • Ensure JavaScript is loading properly.
    • Check if scripts are deferred or blocked.
    • Use useEffect to verify hydration state.

3. Window or Document is Undefined

  • Issue: Code referencing window or document fails during SSR because these objects are only available in the browser.

  • Fix:

    • Check for the environment before accessing window or document:
    useEffect(() => {
      if (typeof window !== "undefined") {
        console.log("Client-side rendering");
      }
    }, []);

4. Styles Not Loading Properly

  • Issue: CSS-in-JS libraries (like styled-components) might not apply styles correctly in SSR.
  • Fix:
    • Ensure you configure SSR correctly in your styling library.
    • Use Next.js’s built-in styled-components support if applicable.

Additional Considerations for SSR in React

Performance Optimization

  • Use code splitting (React.lazy) to load components only when needed.
  • Cache server responses to reduce rendering time.
  • Optimize database queries in SSR to prevent slow response times.

Handling Authentication in SSR

  • Use cookies or headers to pass authentication tokens securely.
  • Store authentication data in a global state like Redux or Context API to avoid unnecessary re-fetching.

Alternatives to SSR

  • Static Site Generation (SSG): Pre-builds pages at compile time for even better performance (e.g., Next.js getStaticProps).
  • Incremental Static Regeneration (ISR): Updates static pages periodically without rebuilding the entire site.

Conclusion

Server-Side Rendering (SSR) in React offers advantages like better SEO and faster first-paint, but it comes with complexities like hydration issues and performance concerns. Understanding when to use SSR vs. CSR and how to handle common hydration problems ensures smoother development and a better user experience.

For production-ready SSR in React, frameworks like Next.js simplify the process with built-in optimizations.