How To Improve React App Performance with SSR and Rust [Part I: SSR]
Tags: react, javascript, webperf, webdev
Published: 2020-02-14 21:33:25

>Server-side rendering (SSR) is technique when content for a web page is rendered on the server using JavaScript.

SSR speeds initial loading up that, in turn, helps increase Google PageSpeed Performance score for SPA (`React.js`, `Vue.js`, `Angular`, etc.). Usual approach is to use Node.js web server such as `Express.js` and render on the server in the fly. We all know that Node.js is quite fast, but we want to boost our web app to maximum available speed.

##Does SSR require Node.js?

Commonly, `React.js` Apps have static numbers of routes. So, we can easily make rendered pages at the same stage when JavaScript bundles are generating. So, we can use these static HTML files with any web server that lets to implement routing logic. That basically means by getting a route, e.g: test.com/test the web server returns according an HTML file that is created by using `ReactDOMServer.renderToString()`

##React App Setup

Let's first start with preparing front-end side as an example will be using `React.js`.
We need to create a simple React.js website with three routes. At first, we should create a file with Routes for using it in React app and web server.

```javascript
const ROUTES = {
  HOME_PAGE: '/',
  ABOUT: '/about',
  CONTACT: '/contact',
};

// Keep it as CommonJS (Node.js) export
module.exports = ROUTES;
}
```

Normally, React.js app optimisation starts with code splitting. In our case is good to split code by routes. Good choice for it is using `@loadable/component`. This library has ready to go solution for SSR that is located in the `@loadable/server` npm package. The first package allow to use dynamic import inside React, therefore `Webpack` can split bundle by these imports.

```javascript
const HomePage = loadable(() => import('./pages/home/HomePage'), {
  fallback: <Loading />,
});
```

In addition, we should use `StaticRouter` instead of `BrowserRouter` for SSR side. To achieve this we can have two different entry points: `App.jsx `and `AppSsr.jsx`, the last one includes:

```javascript
import { StaticRouter } from 'react-router';

import Routes from './Routes';

function App({ route }) {
  return (
    <StaticRouter location={route}>
      <Routes />
    </StaticRouter>
  );
}});
```

Next task for us is creating a function that creates an HTML file by route. Using @loadable/server code looks like that:

```javascript
const { ChunkExtractor } = require('@loadable/server');

async function createServerHtmlByRoute(route, fileName) {
  const nodeExtractor = new ChunkExtractor({ statsFile: nodeStats });
  const { default: App } = nodeExtractor.requireEntrypoint();

  const webExtractor = new ChunkExtractor({ statsFile: webStats });

  const jsx = webExtractor.collectChunks(React.createElement(App, { route }));
  const innerHtml = renderToString(jsx);
  const css = await webExtractor.getCssString();
  const data = {
    innerHtml,
    linkTags: webExtractor.getLinkTags(),
    styleTags: webExtractor.getStyleTags(),
    scriptTags: webExtractor.getScriptTags(),
    css,
  };

  const templateFile = path.resolve(__dirname, './index-ssr.ejs');

  ejs.renderFile(templateFile, data, {}, (err, html) => {
    if (err) {
      console.error(err);
      throw new Error(err);
    } else {
      const htmlMini = minify(html, {
        minifyCSS: true,
        minifyJS: true,
      });
      fs.writeFile(`${distPath}/${fileName}.html`, htmlMini, 'utf8', () => {
        console.log(`>>>>>>>>>>>>>>>> for Route: ${route} ----> ${fileName}.html --> Ok`);
      });
    }
  });
}
```

So, now we can go throw our routes and create all HTML files that we need:


```javascript
async function generateSsr() {
  process.env.NODE_ENV = 'production';

  Object.entries(ROUTES).forEach(async ([key, value]) => {
    routes.push([
      value.substr(1),
      key.toLowerCase(),
    ]);
    try {
      await createServerHtmlByRoute(value, key.toLowerCase());
    } catch(e) {
      console.error(e);
      process.exit(1);
    }
  });
}
```

As you noticed in the `createServerHtmlByRoute` function there is an HTML template which we are using for putting into it generated HTML and CSS:

```HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <style id="css-server-side"><%- css %></style>
  <%- linkTags %>
</head>
<body>
  <div id="app"><%- innerHtml %></div>
  <%- scriptTags %>
  <%- styleTags %>
</body>
</html>
```

It looks like this approach is not perfect because in this case, each HTML file contains some CSS duplicates, such as CSS libraries or common CSS. But it is the simplest solution for speed initial loading up. Another one is an `HTTP/2` feature - `Server Push` when a Web Server pushing CSS files with HTML together.

Finally, after running the build script we should get HTML files for all routes and default - index.html:

![File List](https://dev-to-uploads.s3.amazonaws.com/i/yka8kx3rswnj2k47s1gc.png)

Full example is located in the [GitHub repository](https://github.com/pagespeed-green/react-ssr)

Thus, we got everything that we need from `JavaScript/React.js` side. The next article will cover Rust `Web server` implementation.

You can check how this approach works in production by getting Google PageSpeed Insights Performance score for [PageSpeed Green](https://pagespeed.green) website.
Happy coding!

---

* [Website vector created by slidesgo - www.freepik.com](https://www.freepik.com/free-photos-vectors/website)