What’s Server Side Rendering and do I need it?

Introduction of modern JavaScript frameworks / libraries that focus on creating interactive websites or Single Page Applications the way that pages are displayed to a visitor has changed a lot.

Before the advent of applications fully generated by JS, in the browser, HTML was returned in response to the HTTP call — be it by returning a static HTML content file, or by processing the response via server side languages (such as PHP, Python or Java) and responding in a more dynamic way.

A solution like this allows us to deliver responsive sites that work a lot faster than standard request-response model by removing the “request travel time”.

A typical response sent by the server when requesting a React site will look something like this:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="shortcut icon" href="/favicon.ico">
<title>React App</title>
</head>
<body>
<div id="root"></div>
<script src="/app.js"></script>
</body>
</html>

After fetching this response our browser will also fetch the app.js “bundle” which contains our application and after a second or two render the complete page.

At this moment we can use our browser HTML Inspector to view the complete rendered HTML, but when we view the actual source we will see nothing more than the above HTML code.

Why is this an issue?

While this behavior will not be a problem for most our users or when developing the application it will become an issue if:

  • the end user is using a slow internet connection, especially mobile connection,
  • the end user is running an underpowered device, for example an older generation mobile device,

If your target demographic falls into one of those groups they will have a bad experience using the site — for example will have to wait a lot longer, staring at the “Loading …” (or even worse — a blank screen) message.

“OK, but my target demographic is not in any of those groups, should I still care?”

There’s one more important thing you should consider when going with a client-side rendered app: search engines and social networks presence.

Currently, of all the search engines only Google has limited capabilities to render and JS site before indexing it. In addition, while Google will be able to render the index page of your website it is known to have issues navigating around sites with a router implementation.

This means that your site will have very hard time trying to get top position in the search results in anything but Google.

The same issue is visible in social platforms such as Facebook — when sharing a link to your site neither the title nor the thumbnail will render properly.

How to solve the issue

There are a few ways we can solve the issue.

A — Consider having your key pages as static

When you’re creating an platform that requires the users to login, and not providing the content to not-signed in users you might decide to create your public facing sites (like the index, “about us”, “contact us” etc.) pages as static HTML, and not have them rendered by JS.

Since your content is gated by login requirements it will not be indexed by search engines or shared in social media.

B — Generate parts of your application as HTML pages when running the build process

Libraries like react-snapshot can be added to your project, used to generate HTML copies of your application pages and save them to a specified folder. You can then deploy this folder alongside your JS bundle. This way, the HTML will be served along with the response allowing your site to be accessible by users with JavaScript disabled, search engines etc.

In most cases, configuration of react-snapshot is as simple as adding the library to your project, and altering the build script by calling it at the end:

"build": "webpack && react-snapshot --build-dir static"

The downside of this solution is that all the content we want to generate must be available at build time — we can’t query any APIs to get it, we can’t pre-generate content that depends on user provided data (e.g. as URL).

C — Create a server-side rendered application in JS

One of the big selling point of the current generation of JS applications is the fact, that they can be ran on both the client (browser) and on server — this allows us to generate HTML for pages that are more dynamic — which content is not known at build time. This is often referred to as “isomorphic” or “universal” application.

Two of the most popular solutions that provide SSR for React are:

Create your own custom SSR implementation

Important: If you’re willing to try to and create your own SSR implementation for your React applications, you’re going to need to be able to run a node backend for your server. You will not be able to deploy this solution to static host like github pages.

First step we’re going to take is to create an application just like you would with any other React application.

Let’s create our entry point:

// index.js
import React from 'react';
import { render } from 'react-dom';
import App from './App.js';
render(<App />, document.getElementById('root'));

And the App component:

// App.js
import React from 'react';
const App = () => {
return (
<div>
Welcome to SSR powered React application!
</div>
);
}

And our “shell” to load our application:

// index.html
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<div id="root"></div>
<script src="/bundle.js"></script>
</body>
</html>

As you see our application is quite simple. We’re not going to go through all the steps needed to generate the proper webpack+babel setup in this article.

If you run the application in its current state, you will notice the welcome message on screen, and viewing the source you will see the content of index.html file, without the actual welcome message. To solve this issue let’s add server side rendering. First, we will need to add 3packages:

yarn add express pug babel-node --save-dev

Express is a powerful webserver for node, pug is a templating engine we can use with express, and babel-node is a wrapper for node, which allows it to perform code transpilation on the fly.

First, we will make a copy of our index.html file, and save it as index.pug:

// index.pug
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<div id="root">!{app}</div>
<script src="bundle.js"></script>
</body>
</html>

You’ll notice the file is basically the same, save for !{app} being inserted into the HTML. This is a pug variable, which will be later on replaced by the actual HTML.

Let’s now create our server:

// server.js
import React from 'react';
import { renderToString } from 'react-dom/server';
import express from 'express';
import path from 'path';
import App from './src/App';
const app = express();
app.set('view engine', 'pug');
app.use('/', express.static(path.join(__dirname, 'dist')));
app.get('*', (req, res) => {
const html = renderToString(
<App />
);
  res.render(path.join(__dirname, 'src/index.pug'), {
app: html
});
});
app.listen(3000, () => console.log('listening on port 3000'));

Let’s step through the file.

import { renderToString } from 'react-dom/server';

react-dom library contains a separate, named export renderToString which works similarly to the render we know, but instead of rendering to the DOM, it renders the HTML as string.

const app = express();
app.set('view engine', 'pug');
app.use('/', express.static(path.join(__dirname, 'dist')));

We’re creating a new express server instance, and letting it know we want to use the pug template engine. We could get away with reading the file and doing a search & replace, but that’s really ineffective and can cause issues with multiple access to the file system or caching issues.

In the last line, we’re instructing express to look for file in the dist folder, and if a request (e.g. /bundle.js) matches a file, that is present in that folder, reply with it.

app.get('*', (req, res) => {
});

Now in turn we ask express to add a handler for any unmatched URL — this includes our non-existing index.html file (remember, we renamed it to index.pug and also it’s not available in the dist folder).

const html = renderToString(
<App />
);

Using renderToString we render our application. This code looks exactly like our entry point, but it does not have to.

res.render(path.join(__dirname, 'src/index.pug'), {
app: html
});

Once we have the rendered HTML, we tell express to respond by rendering the index.pug file, and replacing the app variable, with the HTML we received.

app.listen(3000, () => console.log('listening on port 3000'));

Last part is actually starting the server, and making it listen on port 3000.

All that’s left to do is to add a proper script topackage.json:

"scripts": {
"server": "babel-node server.js"
}

Now, once we call yarn run server we should receive a confirmation, that the server is indeed running. Navigate your browser to http://localhost:3000 where you should again see your application. But this time if you view the source you should see:

<!doctype html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<div id="root">div data-reactroot="">Welcome to SSR powered React application!</div></div>
<script src="bundle.js"></script>
</body>
</html>

If that is the case, it means that server-side rendering is working as expected, and you can start to grow your application!

Why do we still need bundle.js?

In case of our super-simple application, inclusion of bundle.js is not needed — without it, our application would still work. But in case of real-life applications, you’d still want to include the file.

This allows the browsers that know how to handle JavaScript to take over and perform the future interaction with your page on the client side, and those that do not know how to parse JS to navigate the page with the actual HTML returned by the server.

Things to keep in mind

While SSR might look simple enough, when developing the application you need to watch out for some topics, which might not be initially clear:

  • any state generated on the server side will not be passed to the client application state; that means that if your backend fetches some data and uses it to render HTML, it will not be placed in the this.state that the browser sees
  • componentDidMount is not called on the server — this means that none of your data-fetching that you’re used to place there will not be called; this is generally a good thing, as you should be providing the data as props if you need it. Remember that you need to delay your render (calling res.render) until the data is fetched, which might introduce some delay for your visitors
  • if you’re using a react router (e.g. @reach/router or react-router) you need to make sure that the proper URL is passed to the application when it’s rendered on the server — be sure to check the proper documentation!