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 applications 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, since 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">
  <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>
      
      





By selecting this response, our browser will also select the "package" app.js



containing our application, and after a second or two, it will render 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:



  • -, ,
  • , ,


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 social media presence .



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 through sites that have a router.



This means that it will be very difficult for your site to climb to the top of the results of any search engine other than Google.



The same problem can be seen on social networks, for example, on Facebook - if a link to your site is shared, 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 without logging in to the system, the content is not provided to the visitor, you can try to leave static (written in HTML) 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 to your project ; 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 also 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 change 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 both on the client (browser) and on the server. This makes it possible to generate HTML for pages that are more dynamic, those whose content is not yet known at build time. Such 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 (
    <div>
      Welcome to SSR powered React application!
    </div>
  );
}
      
      





And also a "wrapper" 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 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. Looking at the source code, you will see the contents of the file index.html



, but 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
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
  </head>
  <body>
    <div id="root">!{app}</div>
    <script src="bundle.js"></script>
  </body>
</html>
      
      





As you can see, the file has not changed much, except for what is now inserted into the HTML !{app}



. This is a variable pug



that will later be replaced by the actual 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('/', 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 renderToString



that works like the one we know render



, but it does not render the DOM, but HTML as a string.



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







We create a new express server instance and tell 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 the "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.



In the last line, we tell express to look for a file in a directory dist



, and if the request (for example /bundle.js



) matches a 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 the help renderToString



we display 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



with the HTML we received.



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





Finally, we ensure that the server starts up and configure it so that it listens on port 3000.

Now we just need to add the required script to package.json



:



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





Now, by calling yarn run server



, we should receive confirmation that the server is indeed running. Go to localhost : 3000 in the browser , where, again, we should see our application. If we look at the source code at this stage, we 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 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 a very 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 cannot parse JS will go to the page with the desired HTML returned by the server.



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:



  • , , . , , HTML, this.state



    ,
  • componentDidMount



    โ€” , , . , , . , ( res.render



    ) , . -
  • react (. @reach/router react-router) , URL, . !



All Articles