FrontendMasters – Intermediate React – Brian Holt : Server Side Rendering

Performance is a central concern for front end developers. We should always be striving to serve the leanest web apps that perform faster than humans can think. This is as much a game of psychology as it is a a technological challenge. It’s a challenge of loading the correct content first so a user can see a site and begin to make a decision of what they want to do (scroll down, click a button, log in, etc.) and then be prepared for that action before they make that decision.

Enter server-side rendering. This is a technique where you run React on your Node.js server before you serve the request to the user and send down the first rendering of your website already done. This saves precious milliseconds+ on your site because otherwise the user has to download the HTML, then download the JavaScript, then execute the JS to get the app. In this case, they’ll just download the HTML and see the first rendered page while React is loading in the background.

While the total time to when the page is actually interactive is comparable, if a bit slower, the time to when the user sees something for the first time should be much faster, hence why this is a popular technique. So let’s give it a shot.

First, we need to remove all references to window or anything browser related from a path that could be called in Node. That means whenever we reference window, it’ll have to be inside componentDidMount since componentDidMount doesn’t get called in Node.

We’ll also have change where our app gets rendered. Make a new file called ClientApp.js. Put in there:

import { hydrate } from "react-dom";
import { BrowserRouter, BrowserRouter as Router } from "react-router-dom";
import App from "./App";

hydrate(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
  document.getElementById("root")
);

This code will only get run in the browser, so any sort of browser related stuff can safely be done here (like analytics.) We’re also using ReactDOM.hydrate instead of ReactDOM.render because this will hydrate existing markup with React magic ✨ rather than render it from scratch.

Because ClientApp.js will now be the entry point to the app, not App.js, we’ll need to fix that in the script tag in index.html. Change it from App.js to ClientApp.js

Let’s go fix App.js now:

// remove react-dom import
// remove BrowserRouter as Router from react-router-dom import

// replace render at bottom
export default App;

We need a few more modules. Run npm install express@4.17.3 to get the framework we need for Node.

Go change your index.html to use ClientApp.js instead of App.js

<script type="module" src="./ClientApp.js"></script>

Now in your package.json, add the following to your "scripts"

"build": "parcel build",
"start": "npm -s run build && node dist/backend/index.js"

And then add this top level item to your package.json:

{
  "targets": {
    "frontend": {
      "source": ["src/index.html"],
      "publicUrl": "/frontend"
    },
    "backend": {
      "source": "server/index.js",
      "optimize": false,
      "context": "node",
      "engines": {
        "node": ">=16"
      }
    }
  }
}

This will allow us to build the app into static (pre-compiled, non-dev) assets and then start our server. This will then let us run Parcel on our Node.js code too so we can use our React code directly in our App as well.

Let’s finally go write our Node.js server:

import express from "express";
import { renderToString } from "react-dom/server";
import { StaticRouter } from "react-router-dom/server";
import fs from "fs";
import App from "../src/App";

const PORT = process.env.PORT || 3000;

const html = fs.readFileSync("dist/frontend/index.html").toString();

const parts = html.split("not rendered");

const app = express();

app.use("/frontend", express.static("dist/frontend"));
app.use((req, res) => {
  const reactMarkup = (
    <StaticRouter location={req.url}>
      <App />
    </StaticRouter>
  );

  res.send(`${parts[0]}${renderToString(reactMarkup)}${parts[1]}`);
  res.end();
});

console.log(`listening on http://localhost:${PORT}`);
app.listen(PORT);
  • [Express.js][ex] is a Node.js web server framework. It’s the most common one and a simple one to learn.
  • We’ll be listening on port 3000 (http://locahost:**3000**) unless a environment variable is passed in saying otherwise. We do this because if you try to deploy this, you’ll need to watch for PORT.
  • We’ll statically serve what Parcel built.
  • Anything that Parcel doesn’t serve, will be given our index.html. This lets the client-side app handle all the routing.
  • We read the compiled HTML doc and split it around our not rendered statement. Then we can slot in our markup in between the divs, right where it should be. Another strategy you can do here is to make an Html.js component that renders the outer shell of your app. This for now suits our needs
  • We use renderToString to take our app and render it to a string we can serve as HTML, sandwiched inside our outer HTML.

This code won’t 404 or 500 on bad requests. It’s a lot involved to do that with React Router v6. See here to learn more.

Run npm run start and then open http://localhost:3000 to see your server side rendered app. Notice it displays markup almost instantly.

🏁 Click here to see the state of the project up until now: server-side-rendering-1

Try to “Disable Javascript” (Developer Tools checkbox) and see. It will look like this:

In “/frontend” only there are statics (non dev). See “dist/frontend/index.html”

<!DOCTYPE html>
<html lang="en">
  <head>
...
    <script type="module" src="/frontend/index.55c9dee4.js"></script>
  </body>
</html>

Streaming Markup

This is all cool, but we can make it better.

With HTTP requests, you can actually send responses in chunks. This is called streaming your request. When you stream a request, you send partially rendered bits to your client so that the browser can immediately start processing the HTML rather than getting one big payload at the end. Really, the biggest win is that browser can immediately start downloading CSS while you’re still rendering your app.

Let’s see how to do this:

// change react-dom import
import { renderToNodeStream } from "react-dom/server";

// replace app.use call
app.use((req, res) => {
  res.write(parts[0]);
  const reactMarkup = (
    <StaticRouter location={req.url}>
      <App />
    </StaticRouter>
  );

  const stream = renderToNodeStream(reactMarkup);
  stream.pipe(res, { end: false });
  stream.on("end", () => {
    res.write(parts[1]);
    res.end();
  });
});
  • Node has a native type called a stream. A stream, similar to a bash stream, is a stream of data that can be piped into something else. In this case, we have a Node stream of React markup being rendered. As each thing is rendered, React fires off a chunk that then can be sent to the user more quickly.
  • First thing we do is immediately write the head to the user. This way they can grab the <head> which the CSS <link> tag in it, meaning they can start the CSS download ASAP.
  • From there we start streaming the React markup to the user.
  • After we finish with that stream, we write the end of the index.html page and close the connection.

🏁 Click here to see the state of the project up until now: server-side-rendering-2