Starting SSR? Here's all you need to know

SSR is one of the most used rendering solutions that improve the load times of dynamic pages. By rendering content on the server, we can reduce time to page visibility and enhance SEO results. But SSR also comes with some potential risks.

In this article, we'll discuss what we can encounter when migrating from a fully client-side rendered (CSR) to a server-side rendering (SSR) approach and how we can overcome them.

Server-side rendering

Server-side rendering (SSR) is an application's ability to display a web page on the server instead of rendering it in the browser. A fully rendered page is sent to the client, the client's JavaScript bundle takes over, allowing rehydration and enabling the Single Page Application (SPA) framework to take over.

Let's see how frameworks like Next.js and Gatsby differ from common client-side apps built with React.

By using React with something like create-react-app, the rendering will happen in the browser. Despite how large your application is, the browser will get an initial HTML document that looks like this:

-- CODE language-html line-numbers --
<p>&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&nbsp;&nbsp;&lt;head&gt;
&nbsp;&nbsp;&lt;!-- Something here like title, etc --&gt;
&nbsp;&lt;/head&gt;
&nbsp;&lt;body&gt;
&nbsp&nbsp;&nbsp;&lt;div id="root"&gt;&lt;/div&gt;
&nbsp;&nbsp;&nbsp;&lt;script src="/static/vendors.chunk.js"&gt;&lt;/script&gt;
&nbsp;&nbsp;&nbsp;&lt;script src="/static/1.chunk.js"&gt;&lt;/script&gt;
&nbsp;&nbsp;&nbsp;&lt;script src="/static/main.chunk.js"&gt;&lt;/script&gt;
&nbsp;&lt;/body&gt;
&lt;/html&gt;
</p>

Single Page Application base HTML skeleton

The HTML page is essentially empty, but it includes some JavaScript scripts. Once the browser downloads and parses those scripts, React will render the page by injecting a bunch of DOM (Document Object Model) nodes. All the rendering happens on the client (browser) — a process known as client-side rendering (CSR).

We could do that rendering on the server as well and then send the user a fully-generated HTML. This way, users would see something while the browser downloads, parses, and executes the JavaScript — a rendering process known as server-side rendering (SSR).

Yes, server-side rendering can improve performance, but the heavy lifting still has to be done on-demand. By requesting your-website.com, React has to convert your React components into HTML, and you'll still be looking at a blank screen while you wait for it. The work is happening on the server, not on the user's computer."

Frameworks like Gatsby and Next.js (in specific configurations) generate the initial HTML of pages ahead of time, in development, and at compile-time as well. When you run the build script (npm run build), it creates 1 HTML document for every route on your project. This way, our React apps can load as quickly as a "vanilla HTML site!"

Code on the client (browser)

The client-side JavaScript has the same React code used to create it at compile-time. It runs on the user's device and builds up a virtual DOM tree of the page. It then compares it to the HTML built into the document — a process known as rehydration.

Rehydration and render are not the same. When props or state change in a standard render, React can settle any variations and update the DOM. In rehydration, React assumes that the DOM won't change — it's just attempting to adopt the existing DOM and make it interactive.

By rendering something different depending on whether we're within the server-side render or not, we'll break the rehydration process.

Sometimes, React can manage this situation. You might have tried this yourself and solved it. But, the rehydration process is optimized to be fast 🏎️, not to catch and correct mismatches 🤷.

Being aware of the critical problems that rehydration mismatches can create, the React team provided feedback messages highlighting the mismatches:

React hydration warning

Why is SSR such a challenge to implement?

Technical challenges

This concept is often known as accidental complexity. It's a fact there are challenges or limitations that we can overcome, but it requires work and using the best and the most appropriate tools.

window APIs usage

This is a problem with SSR — while you can use the browser APIs in the browser, you can't use them on the server because they don't exist. If you try to do window.on the server (in Node.js) it will break. So, you really don't want to use the browser APIs on your code when you're doing SSR.

What can you do about it?

The obvious thing is to eliminate it. Go over your code, find instances of window. and pull them out. But, the reality is that in some cases is unavoidable, and in those cases, you probably need to safeguard that access using something like typeof window !== 'undefined'. A good idea is to wrap all those DOM accesses in a helper library that does these tests and checks to ensure our application doesn't break when running on the server.

Inherent challenges

These problems that are kind of built-in the problem domain, that you can't really eliminate; they're there. You might try to mitigate them and reduce their impact, but it's a challenge you have to face, which's often known as essential complexity.

With CSR

Time to Visible and Time to Interactive are one and the same. You get the blank HTML page, you run the JavaScript code, it generates the HTML (DOM), and its instantly visible and interactive — meaning that if there's a button, you can click the button, if there's a menu, you can click the menu item and so on.

With SSR

The time to visible and time to interactive become distinct. SSR improves the time to visible because you get that HTML with all the visible elements quickly. But, it turns out that the time to interactivity, at best, stays the same.

In fact, in most cases, we actually can even experience some degradation. So, it creates a scenario where you have a visible user interface, but it's either non-interactive or, at best, partially interactive.

When the time to interactive takes considerably longer than time to visible, it induces "rage clicking". This means if there are buttons, people start repeatedly clicking because they're annoyed and nothing responds to their interaction. In other words, it's not soo much a performance issue anymore; it's something you can consider to be a bug in the software. You have a user interface that users are trying to interact with, but it doesn't work.

It's an inherent problem because it's just a fact of what SSR does. It improves visibility; it doesn't improve interactivity.

What can we do about the SSR interactivity problem?

We can try to mitigate it, that is, to reduce its impact. Let's say you're using React.js to render the user interface — you don't have to put all your React rendering under one node; you can break it up and do rendering into multiple roots.

You might split your page so that all the parts above the fold are rendered with one root and the rest are rendered with another. Then you can do a sort of hydration in multiple parts, so you first hydrate the above the fold part and then hydrate the rest.

But remember, this does not eliminate the problem; it only mitigates it. For instance, if somebody scrolls down or loads the pages when they're already scrolled down, they will try to interact with components below the fold and be frustrated.

In most cases, people don't start clicking right away because they tend to look at the page before they start interacting with it. Still, if your time to interactive gets too long, then you definitely have a problem.

Another inherent problem is the HTML diverges. A diverge happens when the HTML generated on the server-side is different from the HTML that's going to be generated on the client-side. It turns out that the hydration process, instead of just hooking in the interactivity, has to replace something that was previously rendered on the server.

There are a couple of reasons why this might happen. One, let's call it unjustified diverges, and think it as bugs. For some reason, the code running on the server generates slightly different HTML than the code running on the client. Maybe it's a timing issue or a version of a library; perhaps it's just a fact that it's a Node.js environment rather than a particular browser being used.

On the flip side of the coin, there are also justified diverges. Consider that your page has the time and date on the top of the screen, and your server happens to be in a different time zone than your client — time will obviously be different, maybe even the date will be different.

Even if that doesn't happen, time should be different when it's rendered on the browser or on the server. This diverge isn't caused by a bug in the code; it's an unavoidable result of the webpage's design. In this case, we need to try to design our pages, so this kind of diverges don't occur.

A better user experience is generally using some placeholder rather than using a wrong UI state and then replacing it with the right UI state after a bit. So, it could be like a spinner or even a blank area with no content, and that content is then filled in later when the browser does its own work.

Justified diverges example

-- CODE language-js line-numbers --
<p>import React from 'react';

&nbsp;export default function SearchResultsHeader() {
&nbsp;&nbsp;const params = new URLSearchParams(window.location.search);
&nbsp;&nbsp;const searchValue = params.get('query') || '';

&nbsp;&nbsp;return (
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;header&gt
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;h2&gtSearch results for: ";{searchValue}"&lt;/h2&gt
&nbsp;&nbsp;&nbsp;&nbsp&nbsp&lt;/header&gt
&nbsp;&nbsp;&nbsp;);
&nbsp;}
</p>

This component is designed to display a heading with the search term. When server-side rendering, we don't have access to the window.location.search variable since there's no window on the server.

Let's develop the same component logic inside a server-side rendering framework. The following example is done with Gatsby via a page component.

-- CODE language-js line-numbers --
<p>import React from 'react';

export default function SearchPage({ location }) {
&nbsp;&nbsp;const params = new URLSearchParams(location.search);
&nbsp;&nbsp;const searchValue = params.get('query') || '';

&nbsp;&nbsp;return (
&nbsp;&nbsp;&nbsp;&nbsp;<>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;header&gt
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;h2&gtSearch results for:"{searchValue}"&lt;/h2&gt
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;/header&gt
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;// remaining page content here
&nbsp;&nbsp;&nbsp;&nbsp;&lt/&gt
&nbsp;&nbsp;);
}
</p>

The above code was done on a Gatsby page component because Gatsby provides the location object as a component prop automatically. But what about having a separate component just like the first React example?

We have at least 2 options, one being able to receive the searchValue via prop on the SearchResultsHeader component, and the other using the browser window.location API when it's available without needing the value itself being passed via props.

In React, we can use the useEffect hook to perform side effects in function components. Code inside the useEffect hook doesn't run when server-side rendering a component, which means we can have code inside of it that can safely access window. APIs on the browser.

-- CODE language-js line-numbers --
<p>import React, { useEffect, useState } from 'react';

export default function SearchResultsHeader(){
&nbsp;const [searchValue, setSearchValue] = useState(&apos;&apos;);

&nbsp;useEffect(() =&gt; {
&nbsp;&nbsp;const params = new URLSearchParams(window.location.search);
&nbsp;&nbsp;setSearchValue(params.get('query') || '');
&nbsp;}, [])

&nbspreturn (
&nbsp;&nbsp;&lt;header&gt;
&nbsp;&nbsp;&nbsp;&lt;h2&gt;Search results for: &quot;{searchValue}&quot;&lt;/h2&gt;
&nbsp;&nbsp;&lt;/header&gt;
&nbsp);
}
</p>

The above useEffect code will run after the component first render on the browser and properly sets the searchValue variable value.

By running this side effect code after the component gets first rendered, we ensure our server-side rendered HTML and our client-side render HTML match and do not break the rehydration process.

Conclusion

SSR can give a very significant performance boost for dynamic sites, precisely the time to visible. It's not trivial, and the time to visible will be smaller than time to interactive. You will have to face that and implement your project so that visitors to your site don't consider it a bug because it is simply not responsive to their interactions.

References

Rui Saraiva
Front-End Developer