Server-Side vs. Client-Side Rendering

#ProgrammingThu Sep 05 2024

Next.js makes it super easy to utilize server-side rendering (SSR). It’s not just about SSR, though—Next.js allows you to blend server-side and client-side rendering (CSR) seamlessly. But what does it mean to "use it well"? And what exactly are SSR and CSR? Let's dive in to find out!

CSR and SSR

CSR (Client-Side Rendering)

When you create an app with create-react-app, you'll notice code in the index.html file that looks like this:

<body>
  <noscript>You need to enable JavaScript to run this app.</noscript>
  <div id="root"></div>
</body>

React, which relies on CSR, initially sends an empty HTML file from the server to the client. Then, it requests and loads the app.js file to display the final app to the user. This method allows the user to navigate within the site without needing new HTML files from the server, resulting in a smooth experience without screen flickers.

However, this also means that the bundled JavaScript file can be quite large, leading to longer initial load times. Plus, because the server initially sends an empty HTML file, SEO performance isn't great. Here’s a quick summary:

  • The time it takes for a user to see the first screen is longer.
  • Not optimal for SEO (Search Engine Optimization).
  • Great user experience for interactive pages since Time to Interactive (TTI) and Time to View (TTV) can occur simultaneously.

SSR (Server-Side Rendering)

In contrast, SSR involves the server fetching all necessary data and generating the HTML file. This approach offers better SEO and faster initial loading times, allowing users to view the site sooner (fast TTV). However, since the JavaScript code is executed after the HTML is loaded, the TTI can lag. Here’s a summary:

  • Users can see the first screen quickly.
  • Good for SEO.
  • Screen flickers can occur due to new HTML files being fetched with each page change.
  • There’s a longer gap between TTI and TTV.

CSR vs. SSR

The main difference between these two methods is where the rendering happens. CSR renders pages on the client side, while SSR does it server-side. With CSR, you need to think about how to break down the final JavaScript bundle to send only what's needed for the user to see the page. With SSR, you need to focus on minimizing the gap between TTI and TTV.

Next.js is the framework that helps leverage the strengths of both rendering methods.

Next.js

In its introduction, Next.js claims to offer a great development environment with plenty of built-in features. These features include support for using SSR and CSR together and pre-rendering functions that are supported for both SSR and Static Site Generation (SSG). If you’re wondering, "Wait, what's SSG?"—don't worry, we’ll get to that!

Using SSR and CSR Together

First, let’s create a Next.js app using the npx create-next-app command. I love TypeScript, so I’ll use it for this project. After running the command, you'll be asked to name your project—let’s go with "next-app."

create next app

Go into the `index.tsx` file in the pages folder, clear out the default Next.js template code, and input `suzie.world`. Run the app and check the docs tab in the Network panel to see the HTML file with your input being rendered server-side.

html response

By using next/link or next/router, you can enable CSR for page transitions. I created a hello.tsx file in the pages folder and used next/link in index.tsx to set up routing.

<ul>
  <li>
    <Link href="/">
      <a>Home</a>
    </Link>
  </li>
  <li>
    <Link href="/hello">
      <a>Hello</a>
    </Link>
  </li>
</ul>

If you navigate from the index page to the hello page while monitoring the network tab, you’ll see that no new requests are made during the page transition.

This shows how the first page can be rendered server-side, while subsequent routing can utilize client-side rendering, allowing SSR and CSR to work together seamlessly.

Pre-Rendering with SSR and SSG

Next.js documentation introduces SSG as "Static-Generation." By default, Next.js pre-renders every page, and it supports pre-rendering in two ways: SSG (Static Generation) and SSR. The difference lies in when the HTML files are generated.

Static Generation (SSG)

HTML files are generated at build time and reused for every request.

  • getStaticProps
    • Used when a page’s content relies on external data. This async function makes Next.js pre-render the page at build time using the props returned by getStaticProps.
import { GetStaticProps } from 'next'

export const getStaticProps: GetStaticProps = async (context) => {
  return {
    props: {}
  }
}
  • getStaticPaths
    • Used when a page’s path relies on external data. This function makes Next.js pre-render all paths specified by getStaticPaths statically.
import { GetStaticPaths } from 'next'

export const getStaticPaths: GetStaticPaths = async () => {
  return {
    paths: [
      { params: { ... } }
    ],
    fallback: true or false
  };
}

Server-Side Rendering (SSR)

A new HTML file is generated for each request.

  • getServerSideProps
    • Used for pages that need SSR. When this function is used, Next.js pre-renders the page on each request using the data returned by getServerSideProps.
export async function getServerSideProps(context) {
  return {
    props: {}
  }
}

The documentation's section on When should I use getServerSideProps? states that this function should only be used when a page must be pre-rendered on each request to fetch data at request time. For pages with data that needs to update dynamically, but without needing pre-rendering, fetching data client-side is recommended. Of course, they subtly suggest their product SWR, but I personally prefer React Query.

Conclusion

SPA, CSR, SSR, SSG, TTI, TTV... The acronyms in the world of the internet are endless. While using Next.js at work and studying its pre-rendering features, I found myself exploring the history from the SSR days of the 1990s to the SPA era of the 2020s. Developing after so many of the original inconveniences have been resolved means that we often have access to tools that easily solve these problems. That’s why it’s so important to understand what problem a good tool solves and how to use it properly to make the most of it.

So, what I wanted to say is... once you’ve chosen the right tool, happy coding with these great tools!