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

Author profile picture

@pagespeed.greenAlex Tkachuk

Web Performance Improver :-)

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;
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 />,
});
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:
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:
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:
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:
<!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:
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!

Tags

The Noonification banner

Subscribe to get your daily round-up of top tech stories!