Server Side Rendering is Magical

Table of Contents

What is Server Side Rendering?

Why use Server Side Rendering?

How to Implement Server Side Rendering with React

Code for this tutorial can be found on GitHub

What is Server Side Rendering?

Server side rendering is the ability for the server to immediately return a fully rendered HTML page for the client. A simple static file server might return the exact contents of an index.html file on the server.

A common practice is to have some kind of template language/engine (e.g. Markdown, pug, or jinja) that allows for more advanced control over the html text that is output.

With client side libraries like React, generally a simple index.html file is returned regardless of what URL is accessed.

<!-- index.html -->

<html lang="en">
<head>
    <script src="app.js" async defer></script>
</head>
<body>
    <div id="root"></div>
</body>
</html>

Once the app.js script above gets loaded, it immediately fires up the React library which in turn bootstraps itself using the #root element.

// index.tsx

import React from "react";
import ReactDOM from 'react-dom';
import { App } from './App';

ReactDOM.render(<App />, document.getElementById('root'));

The above code tells React to render the App component beneath the #root div. You will also likely use react-router to handle navigating to different URLs. With this method, react-router prevents the browser from fetching a new page from the server whenever a link is clicked on the page.

In the context of React, server side rendering means that when accessing a specific URL the html that is returned is already populated with what the page should display.

Take a very simple React application:

// App.ts
import React from "react";

export const App: React.FC = () => {
    return <h1>Hello World!</h1>
}

When server side rendering is properly implemented, instead of the index.html we saw above, the server would instead return:

<!-- index.html -->

<html lang="en">
<head>
    <script src="app.js" async defer></script>
</head>
<body>
    <div id="root">
        <h1 data-reactroot="">Hello World!</h1>
    </div>
</body>
</html>

Now that I’ve explained what server side rendering is let’s go over why and when you should use it and finally how to actually implement.

Why use Server Side Rendering?

Client side web applications are all the rage of Web 2.0. They’ve helped make web applications more interactive by putting the user’s browser in control of what information is shown on the page.

Unfortunately, this comes with some cons. A web page now needs to fetch an index.html file, parse the html file, then fetch one JavaScript file (often more than one), then parse the JavaScript, execute the JavaScript to figure out what to render on the page, and usually send several more requests to fetch the actual information required for the current page.

In this post, I will use a simple blog website as an example. When using a client side library like React for building a blog, there are a few implications regarding site performance and search engine optimization that can be improved with server side rendering.

Page Performance

This is one of the top reasons to implement SSR. With SSR the very first request a browser makes returns the exact html that should be displayed. This saves potentially mutliple round trips of fetching other resources and further speeds things up because your bundled JavaScript code does not need to parse and execute in order to determine what to show on the page.

For pages with dynamic content based on individual users, adding server side rendering potentially puts quite a bit of strain on your server(s) as every request requires the server to generate a unique response. The bigger gains come from instances where the rendered html will be the same for multiple clients. In our blog example, this is exactly the case. A single post is the same for everyone that goes to view it. We won’t cover this in this post, but it would then be possible to cache the rendered html so that it doesn’t need to be re-rendered on every request.

Support Browsers With JavaScript Disabled

There is a small group of Internet users that either disables JavaScript or accesses the Internet with a device that has limited computing capabilities. Pre-rendering content with server side rendering makes it possible for these users to still access the content of you application in some cases (albeit without some of the interactive bits).

Search Engine Optimization (SEO)

Web Crawlers also fall in this category as they generally do not run client side javascript when crawling a web page. If your page requires JavaScript to view the content these crawlers will be unable to index you web page. This can negatively impact your Search Engine Optimization, causing your website to not show up in search results.

Most social media websites use the Open Graph protocol to determine how to show those fancy preview images whenever you share a link on social media. Without server side rendering, it’s not possible to have this information populate dynamically. For our blog example, this means that the link preview for a specific post will return the same meta information as the home page. So instead of a nice preview showcasing the specific post we are linking to, it instead displays generic information about the website. In a future post, I will go into more details about how to add these link previews on top of the server side rendering we’re implementing here.

How to Implement Server Side Rendering with React

Initialize Project

Create a new folder to house this project:

mkdir react-ssr

Change directory

cd react-ssr

Initialize an npm package

npm init

Create Simple React Application

Install react and react-dom

npm i react react-dom

Create a folder to house the React client code

mkdir -p client/src

Create a file called index.tsx in client/src

// index.tsx
import React from "react";
import ReactDOM from 'react-dom';
import { App } from './App';

ReactDOM.render(<App />, document.getElementById('root'));

Create App Component App.tsx in client/src

// App.tsx
import React, { useEffect, useState } from "react";

export const App: React.FC = () => {
    const [clientMessage, setClientMessage] = useState("");
    
    useEffect(() => {
        setClientMessage("Hello From React");
    })

    return <>
        <h1>Hello World!</h1>
        <h2>{clientMessage}</h2>
    </>
}

Bundle the React App with esbuild

Install esbuild

npm i --dev esbuild

Add build script to package.json

{
  "name": "react-ssr",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "client:build": "esbuild client/src/index.tsx --bundle --outfile=built/app.js",
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "react": "^17.0.2",
    "react-dom": "^17.0.2"
  },
  "devDependencies": {
    "esbuild": "^0.14.0",
  }
}

Create Starter Express.js Application

Create a folder to house the express.js application

mkdir -p server/src

Create a file called server.tsx in server/src

// server.tsx
import express from "express";

const app = express();

app.get('/', (req, res) => {
    const html = `
        <html lang="en">
        <head>
            <script src="app.js" async defer></script>
        </head>
        <body>
            <div id="root"></div>
        </body>
        </html>
    `
    res.send(html);
});

app.use(express.static("./built"));

app.listen(4242);

Bundle the express.js server with esbuild

Add server:build and start scripts to package.json

{
  "scripts": {
    "client:build": "esbuild client/src/index.tsx --bundle --outfile=built/app.js",
    "server:build": "esbuild server/src/server.tsx --bundle --outfile=built/server.js --platform=node",
    "start": "node built/server.js"
  },
}

Build the Server Bundle

npm run server:build

Run the Server

npm start

You can now open your browser to http://localhost:4242 and see the “Hello World!” message.

Add Server Side Rendering

// server.tsx
import express from "express";
import * as ReactDOMServer from 'react-dom/server';
import { App } from "../../client/src/App";

const app = express();

app.get('/', (req, res) => {
    const app = ReactDOMServer.renderToString(<App />);

    const html = `
        <html lang="en">
        <head>
            <script src="app.js" async defer></script>
        </head>
        <body>
            <div id="root">${app}</div>
        </body>
        </html>
    `
    res.send(html);
});

app.use(express.static("./built"));

app.listen(4242);

This imports the App component and renders it to a string using the react-dom/server library. We then place that rendered string inside our #root div and return the resulting html.

Re-build the server with npm run server:build. Then restart the server with npm start.

You can use curl to confirm this works as expected by running curl http://localhost:4242.

You should see the following:

<html lang="en">
<head>
    <script src="app.js" async defer></script>
</head>
<body>
    <div id="root"><h1 data-reactroot="">Hello World!</h1></div>
</body>
</html>

Note that the “Hello From React” message is missing. This is because the renderToString method will not run the useEffect logic.

If we refresh the page in the browser we should see the correct clientMessage correctly.

Switching From ReactDOM.render to ReactDOM.hydrate

While this seems to be working as expected, there is one final change to make. The server returns the rendered html and then once the app.js script is loaded it’s going to re-render the entire App component and overwrite what’s already there.

// index.tsx
ReactDOM.render(<App />, document.getElementById('root'));
ReactDOM.hydrate(<App />, document.getElementById('root'));

With this change, React will re-use the existing markup and attach any event listeners to the existing elements.

In our small example, this won’t make much of a difference, but in a large application this could save a lot of re-rendering.

Wrap-Up

Congratulations! You now have a react application with server side rendering. In my experience, it’s much easier to start off with server side rendering than implement it after the fact. Now that you know the fundamentals behind it, you should be able to apply these principles to an existing React application as well.

Additional Resources

https://www.digitalocean.com/community/tutorials/react-server-side-rendering