DEV Community

Cover image for How To Improve React App Performance with SSR and Rust [Part I: SSR]
Olex Tkachuk
Olex Tkachuk

Posted on • Edited on • Originally published at hackernoon.com

How To Improve React App Performance with SSR and Rust [Part I: SSR]

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.

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

// Keep it as CommonJS (Node.js) export
module.exports = ROUTES;
}
Enter fullscreen mode Exit fullscreen mode

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.

const HomePage = loadable(() => import('./pages/home/HomePage'), {
  fallback: <Loading />,
});
Enter fullscreen mode Exit fullscreen mode

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

import { StaticRouter } from 'react-router';

import Routes from './Routes';

function App({ route }) {
  return (
    <StaticRouter location={route}>
      <Routes />
    </StaticRouter>
  );
}});
Enter fullscreen mode Exit fullscreen mode

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

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`);
      });
    }
  });
}
Enter fullscreen mode Exit fullscreen mode

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

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);
    }
  });
}
Enter fullscreen mode Exit fullscreen mode

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

<!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>
Enter fullscreen mode Exit fullscreen mode

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

Full example is located in the GitHub repository

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 website.
Happy coding!


Top comments (0)