React server-side rendering with Webpack

There are times when the initial blank html page being download for a React application is not perfect. One reason might be Search Engine Optimization, another might be a slower initial render, especially on mobile devices.

Search Engine Optimization (SEO) is a good reason you might want to do server-side rendering. While the Google bot executes JavaScript these days, and can potentially index your React application, other search engine bots don’t do so. The result is they just see the empty div where your application will be rendered on the real client and nothing worth indexing at all. The end result is no traffic from them. So, if you want to maximize traffic to your React application using SEO Server-side rendering is a must have.

A slower initial render is another reason to use Server-side rendering. This is especially true with slow mobile connections but even helps on fast desktops. With a standard React application, the browser downloads the index.html, parses and renders it. This results in a blank screen as there is no content. While this is done the React application JavaScript is downloaded, executed and injected into the DOM. Only now is there a meaningful page to display in the browser. With a Server-side rendering style application, the initial HTML page already contains all, or most, of the markup so it can be displayed much faster. The initial page might not be fully functional but it appears to be there faster to the user.

Of course, it isn’t all just good. Server-side rendering means there is more work to be done on the server and thus a slower response. It just appears to be faster.

 

Doing Server-Side Rendering with Create React App

There are several approaches to doing Server-side rendering with a React application generated using Create React App (CRA). One would be to use the babel-node utility from Babel-CLI to parse the JSX and render it at runtime. This is possible but using babel-node is not recommended for production use. Another issue with this is that common Webpack practices, like importing CSS and images is not supported by babel-node. You can go without but even a standard CRA generated application does this and already fails.

A much better approach would be to use Webpack to generate two JavaScript bundles, one for use with Server-side Rendering and a second for use in the browser. There is a need for two different bundles because of the difference in the execution environment. One will execute in NodeJS on the server, the other in the browser on the user’s machine.

Webpack actually supports exporting multiple configurations but as the Webpack config is contained in React-Scripts we can’t change it without ejecting. As ejecting adds a lot of maintenance work to my applications I would rather not do that. A better approach is to use the standard Webpack config and update it to make it suitable for a Server-side rendering bundle.

There are just a few changes to be made for this to work. Most of those are about the NodeJS execution environment and a few to prevent some output from being generated.

const HtmlWebpackPlugin = require("html-webpack-plugin");
const ManifestPlugin = require("webpack-manifest-plugin");
const SWPrecacheWebpackPlugin = require("sw-precache-webpack-plugin");

const config = require("react-scripts/config/webpack.config.prod");

config.entry = "./src/index.ssr.js";

config.output.filename = "static/ssr/[name].js";
config.output.libraryTarget = "commonjs2";
delete config.output.chunkFilename;

config.target = "node";
config.externals = /^[a-z\-0-9]+$/;
delete config.devtool;

config.plugins = config.plugins.filter(
  plugin =>
    !(
      plugin instanceof HtmlWebpackPlugin ||
      plugin instanceof ManifestPlugin ||
      plugin instanceof SWPrecacheWebpackPlugin
    )
);

module.exports = config;

Now we can create the SSR bundle using webpack –config ./webpack.ssr.config.js which can be automated using the build script in the package.json.

{
  "name": "server-side-rendering-with-create-react-app",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "react": "^16.1.1",
    "react-dom": "^16.1.1",
    "react-scripts": "1.0.17"
  },
  "scripts": {
    "start": "react-scripts start",
    "start:ssr": "node ./server/",
    "build": "react-scripts build && npm run ssr",
    "ssr": "cross-env NODE_ENV=production webpack --config ./webpack.ssr.config.js",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  },
  "devDependencies": {
    "cross-env": "^5.1.1"
  }
}

An Express server to serve the React application

Creating a simple NodeJS based Express server to host and server the rendered React application is simple. This server will serve the files from the build folder as well as the server-rendered react application.

const path = require("path");
const express = require("express");
const serveStatic = require("serve-static");
const reactApp = require("./react-app");

const PORT = process.env.PORT || 3001;
const app = express();

app.use(reactApp);
app.use(serveStatic(path.join(__dirname, "../build")));

app.listen(PORT, () => {
  console.log(`Server listening on port ${PORT}!`);
});

 

Doing the actual SSR isn’t very hard now. Just load the SSR bundle created above and use the react-dom/server API to render the application. Using the new React 16 renderToNodeStream() API make this a bit more efficient than using the older renderToString() API but not a lot harder.

const fs = require("fs");
const path = require("path");
const router = require("express").Router();

const { renderToNodeStream } = require("react-dom/server");
const React = require("react");
const ReactApp = require("../build/static/ssr/main").default;

console.log(ReactApp)

router.get("/", (req, res) => {
  var fileName = path.join(__dirname, "../build", "index.html");

  fs.readFile(fileName, "utf8", (err, file) => {
    if (err) {
      throw err;
    }

    const reactElement = React.createElement(ReactApp);

    const [head, tail] = file.split("{react-app}");
    res.write(head);
    const stream = renderToNodeStream(reactElement);
    stream.pipe(res, { end: false });
    stream.on("end", () => {
      res.write(tail);
      res.end();
    });
  });
});

module.exports = router;

 

In this case, I added {react-app} as the content of the application div to replace it with the rendered React application on the server.

You can find the sample code on GitHub.

Enjoy.

 

Leave a Reply

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