What is server side rendering and do I need it?

Hello, Habr!

In the new year, let’s start our conversation with you with a seed article on server-side rendering. If you are interested, a more recent publication about Nuxt.js and further publishing work in this direction is possible.

With the advent of modern JavaScript frameworks and libraries, which are primarily intended for creating interactive web pages and single page applications, the whole process of showing pages to the user has changed a lot.

Before the advent of entirely JS-generated apps in the browser, HTML was served to the client in response to an HTTP call. This could be done by returning a static HTML file with the content, or by processing the response using a server-side language (PHP, Python, or Java), and in a more dynamic way.

This solution allows you to create responsive sites that run much faster than standard request-response sites, because it eliminates the time spent by the request “on the way”.

A typical response sent by the server to a request to a site written in React would look something like this:

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

By selecting this response, our browser will also select “package” app.jscontaining our application, and in one or two seconds will display the entire page.

At this point, you can already use the browser’s built-in HTML inspector to view all of the rendered HTML. However, looking at the source code, we will not see anything other than the HTML above.

Why is this a problem?

While this behavior will not be a problem for most of our users or when developing an application, it may become undesirable if:

  • The end user works with slow internet connection, which is especially important for mobile devices,
  • The end user works with low-power devicee.g. with an older generation smartphone

If, from a demographic point of view, your target audience belongs to one of these groups, then working with the site will be inconvenient – in particular, users will have to wait a long time, staring at the sign “Loading …” (or even worse, at a blank screen).

“Okay, but demographically, my target audience is definitely not one of these groups, so should I be worried?”

There are two other things to consider when working with a client-rendered application: search engines and presence on social networks

Today, of all search engines, only Google has some ability to display a site and take into account its JS before displaying a page. In addition, while Google will be able to display your site’s index page, it is known that there may be problems navigating sites that have a router.

It means that it will be very difficult for your site to get to the top of any search engine results other than google.

The same problem can be seen on social networks, for example, on Facebook – if a link to your site is shared, then neither its name, nor the preview picture will be displayed properly.

How to solve this problem

There are several ways to solve it.

A – Try to keep all the key pages of your site static

When a platform site is created, where the user will have to log in with his username, and the content is not provided to the visitor without logging in to the system, you can try leave static (written in HTML) the public pages of your site, in particular, the index, “about us”, “contacts” and do not use JS when displaying them

Since your content is limited by login requirements, it will not be indexed by search engines and cannot be shared on social media.

B – Generate parts of your application as HTML pages during build

You can add libraries such as react-snapshot; they are used to generate HTML copies of your application’s pages and store them in a dedicated directory. This directory is then deployed along with the JS package. Thus, HTML will be served from the server along with the response, and your site will be seen by those users who have JavaScript disabled, as well as by search engines, etc.

Typically, configuring react-snapshot is easy: just add the library to your project and modify the build script as follows:

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

The disadvantage of this solution is this: all the content that we want to generate must be available at build time – we cannot access any APIs to get it, we also cannot pre-generate content that depends on the data provided by the user (for example from a URL).

C – Create a JS application that uses server rendering

One of the biggest selling points of today’s generation of JS applications is that they can be run on both the client (browser) and server. This makes it possible to generate HTML for pages that are more dynamic, those whose content is not yet known at build time. These applications are often referred to as “isomorphic” or “universal”.

The two most popular server-side rendering solutions for React are:

Create your own SSR implementation

Important: if you are going to try to create your own SSR implementation for React applications yourself, you will need to provide a node backend for your server. You won’t be able to deploy this solution to a static host like you would with github pages.

The first thing we need to do is create an application, just like any other React application.

Let’s create an 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 component-application (App):

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

And also a “wrapper” to load our application:

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

As you can see, the application is pretty simple. In this article, we will not go through all the steps required to generate the correct webpack + babel assembly.
If you launch the application in its current state, a welcome message will appear on the screen. By looking at the source code, you will see the contents of the file index.htmlbut the welcome message will not be there. To solve this problem, let’s add server rendering. First, let’s add 3 packages:

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

Express is a powerful web server for node, pug is a templating engine that can be used with express, and babel-node is a wrapper for node that provides on-the-fly transpilation.

First, let’s copy our file index.html and save it as index.pug:

// index.pug

! {app}

As you can see, the file has not changed much, except for the fact that it is now inserted into the HTML !{app}… This is a variable pugwhich will later be replaced with real HTML.

Let’s create our server:

// server.jsimport 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("https://habr.com/", 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 analyze this file in order.

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

The react-dom library contains a separate named export renderToStringworking like the one we know renderbut renders not the DOM, but HTML as a string.

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

We create a new express server instance and inform it that we are going to use a templating engine pug… In this case, we could get by with the usual reading of the file and performing a “find and replace” operation, but this approach is really ineffective and can cause problems due to multiple access to the file system, or problems with caching.

On the last line, we tell express to look for a file in the directory dist, and if the request (ex. /bundle.js) matches the file present in this directory, then return it.

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

Now we tell express to add a handler to every unmatched URL – including our nonexistent file index.html (as you remember, we renamed it to index.pug, and it is not in the directory dist).

const html = renderToString(
  <App />

With help renderToString we render our application. The code looks exactly like the entry point, but such a match is optional.

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

Now that we have rendered HTML, we tell express to render the file in response index.pug and replace the variable app the HTML we got.

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

Finally, we enable the server to start and configure it to listen on port 3000.
Now we just have to add the required script to package.json:

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

Now by calling yarn run server, we should get confirmation that the server is actually running. Go to the browser at localhost: 3000, where we, again, should see our application. If we look at the source code at this stage, we see:

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

If everything looks like this, it means that the server rendering is working as expected, and you can start extending your application!

Why do we still need bundle.js?

In the case of such an extremely simple application, which is considered here, it is not necessary to include bundle.js – without this file, our application will still work. But in the case of a real application, you still need to include this file.

This will allow browsers that can handle JavaScript to take over the work and then interact with your page already on the client side, and those that do not know how to parse JS will go to the page with the desired HTML that the server returned.

Things to Remember

Despite the fact that server rendering looks quite straightforward, when developing applications, you need to pay attention to some topics that at first glance are not quite obvious:

  • Any state generated on the server side will not be passed to the state of the client application. This means that if your backend fetches some data and uses it to render HTML, then that data will not go into this.statethat the browser will see
  • componentDidMount not called on the server – this means that no data fetching operations that you are used to placing there will be called. In principle, this is good because you have to provide the data you want as props. Remember to defer the display (by calling res.render) until the data is selected. Because of this, visitors may notice some delays in the site.
  • if you are going to use react router (ex. @ reach / router or react-router) then you must ensure that the correct URL is passed to the application when it is displayed on the server. Be sure to read about it in the documentation!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *