loading...
Cover image for Best Practices For Keeping Server-Side Rendering Cool

Best Practices For Keeping Server-Side Rendering Cool

m0nica profile image Monica Powell Originally published at aboutmonica.com ・11 min read

Server-side rendering can be powerful but it does require thinking in multiple contexts so it's important to be familiar with some of the common gotchas when developing Server-Side Rendered websites. This article is a written version of a talk I gave at React Rally 2020 on keeping Server-Side Rendering Cool with React Hydration, where I shared some helpful things to keep in mind to render a seamless experience as a Server-Side Rendered (SSR) site transitions from a window-less (server) environment to a browser.

What is Server-Side Rendering (SSR)?

Let's take a step back. First, what is server-side rendering? When a server generates the initial HTML that loads in a browser. Frameworks like NextJS and GatsbyJS support SSR out of the box. Server Side Rendered applications tend to initially load content faster and lead to higher SEO ranking than their Client-Side Rendered counterparts.

There are different types of server-side rendering for example server-side rendering can be used to render every single page request or only the initial page request. NextJS offers two forms of server-side rendering. You may be familiar with Create React App, a default React app boilerplate which does not come with SSR functionality configured out of the box.

What is Client-Side Rendering (CSR)?

Blank page in browser saying "You need to enable JavaScript to run this app"

In contrast to Server-Side Rendering, a website that only supports Client-Side Rendering requires visitors to have HTML enabled to view content on the site. Often visitors will see a largely blank page when visiting a Client-Side rendered application if they do not have JavaScript enabled.

If you look at the DOM in the developer tools of a Create React App (or client-side only rendered application) you'll notice very little HTML markup in the DOM. The markup might resemble something like the below code:

<html>
  <head>
    <!-- SEO/Metadata here -->
  </head>
  <body>
    <div>You need to enable JavaScript to run this app.</div>
    <div id="root"></div>
    <script>
      <!-- all of the JavaScript -->
    </script>
    <script src="/static/js/2.6158a3d8.chunk.js"></script>
    <script src="/static/js/main.ba831a9f.chunk.js"></script>
  </body>
</html>

Generally this markup will include the root where React is injected, a message saying you need to enable JavaScript to run the app as well as script tags that link to the JavaScript that needs to be loaded to hydrate the page.

Overview of SSR (in static context)

step by step illustration walking through react hydration steps

Let's walk through what happens in Server-Side Rendered applications like NextJS or Gatsby when all the pages for the site are statically generated at once in the server.

First, you write the site in React ⚛️ then Gatsby or Next (Static Site Generation) creates a production build of your site using ReactDOMServer, a React server-side API to generate HTML from React. When someone visits your website the first thing they'll see is the HTML generated from the server. JavaScript then loads after the initial page load and the ReactDOM.hydrate() API kicks in to hydrate the HTML page that was rendered from the server with JavaScript. After Hydration the React reconciler APIs take over and the site becomes interactive.

Toggling JavaScript: SSR vs. CSR

Let's compare how a Server-Side and Client-Side rendered applications appear when JavaScript is enabled or disabled. For these two examples, I used Gatsby and Create React App for these technologies.

gif of enabling/disabling JavaScript on Gatsby site

The above image is of a Gatsby site, where when JavaScript is toggled on/off there is very little visible change aside from the image loading as most of the HTML was available without JavaScript.

gif of enabling/disabling JavaScript for site built with create-react-app

In contrast, in the above image of a Create-React-App which uses client-side rendering and the browser is responsible for constructing the initial HTML. Due to this we just see the bare-bones HTML as opposed to a full HTML document when JavaScript is disabled.

My server-side app looks great in development...What could go wrong? 😅

We just looked at an example of Server-Side rendering that looked great in production both with or without JavaScript! What could go wrong? There are some common issues you might run into with Server-Side rendered applications that only occur during the initial hydration process in production such as layout shifts or errors that only appear at build-time.

1. Missing Data on Server-Side

Something helpful to keep in mind is that some data just is not available in the static server context like user or browser-specific data. For example, window size, authentication status, local storage, etc.

screenshot of target.com navigation

In the above image of Target's navigation, you'll see that the store location data, my name, and items in the shopping cart were not available on the initial page load. Once the data was available it hydrated on the page without shifting the layout. Loading patterns like this can be common on server-side rendered applications.

2. Unavailable JavaScript

screenshot of my site header loading with disjointed icons and shifting layout

Let's debug the above hydration issue that caused my site to have multiple unnecessary rendering changes during load. Something huge that is not available on initial load and can cause issues in server-side rendered applications is JavaScript! It's considered a best practice to load CSS before JavaScript therefore you need to consider how the HTML and CSS on a page load BEFORE JavaScript is available since JavaScript is not required for the page to load.

You may end up noticing weird changes on the initial page load that change too quickly to properly inspect - especially if you have a faster internet connection. But there are ways to slow down and really see what is going on. In particular, I'd recommend disabling JavaScript in your browser or using a site like web page test to generate film strip thumbnails that show you exactly how the page is loading step by step.

filmstrips of my site loading with rendering issue

Above is the waterfall I took of the issue on my site before it was resolved. You can see one of the issues is that the size of the FontAwesome icons changes drastically between 96% and 99% loaded which can be a disjointing experience.

crossed out JavaScript icon transitioning to CSS

The solution for getting rid of the resizing of the icons during load involved replicating the final styling with local CSS and removing any dependency on FontAwesome's external CSS which required JavaScript to be available.

I disabled JavaScript which allowed me to see in development that the ways the icons look before they were fully loaded mirrored the app without JavaScript. This led me to realize Font Awesome was using its own styling that was coming in through JS that conflicted with my local CSS style. Since CSS loads before JS, disabling Font Awesome's external CSS (loaded via JavaScript) and replicating the CSS styles I wanted locally resolved the issue

film strip of my site loading without layout  shifts

You'll notice after (above image) removing the dependency on Font Awesome's CSS that the styling of the icons remains consistent as the application loads. I wrote an article with more information about my experience resolving the Font Awesome rendering issues.

3. Immutable Layout

gif of colorful shifting blocks

The previous issue of changing styles is related to a much larger issue of handling layouts on the server-side. Generally, you should avoid unnecessary layout shifts during page load by implementing layouts with placeholder/gap for expected client-side content and avoiding using JavaScript to position or style content instead of CSS. It is common for some data to be unavailable as the page loads, however, you can develop in a way that can handle missing data by leaving room in the UI for the data to load. In the Target navigation example you can see there is no shift as the user/store specific data is loaded.

screenshot of target navigation

4. Strange Conditional Rendering in Server Context

If you write React you may have conditionally rendered content like the below code snippet based on screen size using the MatchMedia API. However, this approach might lead to some unnecessary frustration...

if (small) {
  return <MobileApp />
} else {
  return <DesktopApp />
}

The matchMedia() API can't reliably detect the browser or device size in the server context which can lead to some strange rendering issues as the page loads if the originally set media size doesn't match the actual browser.

It's preferable to use CSS or a library like fresnel which wraps all Media components in CSS instead of MatchMedia in Server-Side rendered applications to layout content. Since CSS loads before JS, styles applied via CSS, unlike JavaScript should visibly match what you expect on page load.

Below is an example of how Fresnel can be used. First, you need to import createMedia from Fresnel then define the breakpoints and export MediaContextProvider from the object created from createMedia to wrap the entire app. Then you can use Fresnel's Media component throughout your app to render components based on the predefined breakpoints.

import React from "react"
import ReactDOM from "react-dom"
import { createMedia } from "@artsy/fresnel"

const { MediaContextProvider, Media } = createMedia({
  breakpoints: {
    sm: 0,
    md: 768,
  },
})

const App = () => (
  <MediaContextProvider>
    <Media at="sm">
      <MobileApp />
    </Media>
    <Media greaterThan="sm">
      <DesktopApp />
    </Media>
  </MediaContextProvider>
)

ReactDOM.render(<App />, document.getElementById("react"))

The final step is inject the CSS in the server by passing mediaStyle into a <style> tag in the head of the document so that CSS can be generated from fresnel markup and be rendered on the server. You can read more about setting up Fresnel for SSR in the Fresnel docs.

5. Error: Window is undefined

gif homer simpson looking under couch cushions looking for something

If you attempt to access browser-specific elements in a server context JavaScript will not be able to resolve those elements.

When building a site you might run into the window is undefined or document is undefined error. This happens when logic within an app assumes the browser window is defined in a server and reference browser-specific elements in the server.

Your first inclination to resolve the undefined Window error might be to write something like:

typeof window !== undefined ? //render component : // return null

However, if your app is using the ReactDOM.hydrate API to transform the site from HTML to the virtual DOM you need to be aware of ReactDOM.hydrate's constraint. ReactDOM.hydrate():

  • 👯‍♂️ expects that the rendered content is identical between the server and the client.

  • 🙅🏾‍♀️ does not guarantee that attribute differences will be patched up in case of mismatches.

The Hydrate API that converts HTML to full-fledged React expects that the content is always identical between the server and client and does not guarantee that matches will be patched up in case of mismatches. Due to this lack of guarantee, it is NOT a good idea to conditionally render based on elements that will differ between the server and client.

Check out this article by Josh W Comeau for examples of visual discrepancies that can occur when
React.Hydrate() fails to reconcile an unexpected difference.{" "}

Safely accessing browser elements enables you to
avoid reconciliation errors when ReactDOM.hydrates a site from HTML to React. In order to avoid issues with the hydration reconciliation process, you can wrap any side effects that rely on the Window or Document in a useEffect hook since that only fires after the component has mounted.

useEffect() Example:

function Example() {
  const [count, setCount] = state(0)
  useEffect(() => {
    document.title = `You clicked ${count} times`
  })
}

This is an example from the React Docs of referencing a browser element, document.title within useEffect(). This code will never be executed on the server as it executes after the React Virtual DOM is available and therefore avoids running into issues with React.Hydrate().

Rule of Least Power

With JavaScript comes great responsibility, sometimes JavaScript just isn't the right tool for the job:

"JavaScript is a powerful language that can do some incredible things, but it’s incredibly easy to jump to using it too early in development when you could be using HTML and CSS instead. Consider the rule of least power: Don’t use the more powerful language (JavaScript) until you’ve exhausted the capabilities of less powerful languages (HTML)." - Iain Bean, Your blog doesn’t need a JavaScript framework

image of my site header dynamically changing headshot images based on the screen size

I recently used the rule of least power to speed up the initial load time of my header and eliminate relying on JavaScript to dynamically load different headers images on my site based on screen size.

I was looking into how to display different images based on screen size and stumbled up HTML art direction which can be used to dynamically load images based on the screen size using HTML srcset attributes instead of JavaScript. Swapping images at different screen sizes can be done with JavaScript or CSS instead of native HTML attributes however using HTML can improve page loading performance as it prevents unnecessarily preloading two images.

The cool thing about the HTML approach is that it can improve page loading performance as it allows the browser to only preload the image that is visible within the viewport. This can be especially beneficial if you need to display multiple images at various places within a site depending on the screen size.

<picture>
  <source media="(min-width: 625px)" srcset="animonica-full.png" />

  <source srcset="animonica-headshot-cropped.png" />

  <img src="animonica-full.png" alt="Illustrated Monica" />
</picture>

In order to set up this functionality in HTML you can use the picture attribute and set media queries on each source image. It will return the first condition the is true and as a fall back it'll return the image from the img tag.

It's also possible to use the art-direction API in Gatsby with gatsby-image which is Gatsby's package for loading and transforming images from GraphQL. I wrote an article with more information about my exploration into using art direction with Gatsby.

Summary

  • In a Server-Side Rendered context it's important to consider how the page loads both when data is and is not available.
  • CSS is the right tool for handling layout, especially in a Server-Side Rendered Application. Using JavaScript for styling in SSR apps can lead to strange loading experiences for some users.
  • It's important to guard references to browser-specific elements like document or window within useEffect() to avoid reconciliation error as the page hydrates to transform SSR apps from HTML to React.

Resources & Further Reading

Below are some resources I recommend if you're looking to further explore the rendering process for server-side rendered React applications.

Discussion

pic
Editor guide