Approaching Server Side Rendering in an Existing React/Redux Application

Stephen England
Apr 12 · 8 min read

There are plenty of SSR tutorials out there. This article goes over how you incrementally work on SSR — how you add it to an existing application. If you want a basic tutorial on how to code a hello world SSR app, I think Redux’s article does a good job.

Why Server Side Rendering (SSR)?

At Spreetail we have a strategy of setting aggressive deadlines. This keeps us focused on minimum viable product — but it also lets us see what we’re capable of. Spreetail.com was no exception — we started the project in October 2018 and started our marketing campaign in April 2019. Usually, with a minimum viable product, fancy features like Server Side Rendering (SSR) would get put off until the main features of the site are completed. So why now?

Search Engine Optimization

When developing a client-side-only application, there are a few disadvantages when it comes to SEO and other web scrapers. Google and Bing can both run JavaScript, but they may not consistently run your JavaScript and wait for the page for as long as you want them to. If you need to make an API call to a product page in order to get the product description and image, you really want the HTML results of consuming that API call to be in SEO.

Facebook does not run JavaScript. This matters when you are sharing links on Facebook. You want to see a nice pretty product description and image, not the homepage’s title and description.

What Facebook users would see if they tried to share a product page
What a product page should look like on Facebook!

Why not Next.js // Meteor // the new-hotness?

We decided to go with a React/Redux application built off of react-boilerplate. Development had already started when I signed on, but I know we considered Next.js instead and ended up choosing not to use it. Looking back, it is much easier to use a framework that already handles server side rendering. I would bias toward using those frameworks in the future.

Performance

To us, this was a secondary concern. However, this is usually the main reason people utilize SSR. It gives users the HTML immediately, rendering something on mobile devices as fast as possible. For a minimum viable product, we knew we would have to circle back around to improve performance. SEO was our top concern.

OK, so we wanted SSR. How do we do it?

The basic “plan of attack”

  1. Webpack Babel build on the Express server
  2. Render a basic JSX with ReactDOM.renderToString(<div>Hello World!</div>)
  3. Add our app!
  4. Create a Redux store
  5. Use our JSX
  6. Run the app, look for errors (global.window, global.document)
  7. Add an initial state in the window
  8. Simulate any behavior in componentDidMount (API calls)

Each step in this plan should be verifiable. It is very easy, when making such wide-sweeping changes to an app, to get lost in what you’ve done. Commit often when you get to a point where things work. Revert often and start over when you are confused or lost.

All of the “basic plan of attack” is covered in the Redux article.

OK — but what else? Where did it get tricky?

React Helmet

When you are doing server side rendering, the basic examples just generate a string that you plop into an index.html file. This won’t work for elements in the <head> tag, so you have to do some additional string manipulation for that. This blog article shows how to solve this problem well. Here is an excerpt:

export default ({ markup, helmet }) => {
return `<!doctype html>
<html ${helmet.htmlAttributes.toString()}>
<head>
${helmet.title.toString()}
${helmet.meta.toString()}
${helmet.link.toString()}
</head>
<body ${helmet.bodyAttributes.toString()}>
<div id="root">${markup}</div>
<script src="/static/client.js" async></script>
</body>
</html>`;
};

Using ReactDOM.renderToNodeStream

So by this point, we had everything working. It was looking great. We were using it in our development environment, doing some testing, when we discovered that once SSR is rendering a page, it blocks all other requests. This is basically a show stopper for us — we have an endpoint, /alive, that tells our infrastructure not to shutdown our server. If we were bombarded with requests, it would fail the /alive check.

The problem was .renderToString(). It is a blocking CPU operation. It can take a while! React has .renderToNodeStream to do http streaming of the content, but it changes what you are doing a lot.

If you are a front end developer, you likely haven’t had to deal with HTTP streaming before. It can be intimidating, but it basically is just handling a few events. Here is what ours looks like:

const renderReactApp = (app, stylesheet, res) =>
new Promise((resolve, reject) => {
const stream = stylesheet.interleaveWithNodeStream(renderToNodeStream(app));
stream.on('data', chunk => res.write(chunk));
stream.on('end', () => resolve());
stream.on('error', reject);
});
const { beforeHead, afterHead } = await getIndexHtmlParts;
res.write(`${beforeHead}
${helmetContext.helmet.title.toString()}
${helmetContext.helmet.meta.toString()}
${helmetContext.helmet.link.toString()}
${helmetContext.helmet.script.toString()}
${afterHead}<div id="app">
`
);
await renderReactApp(reactApp, styledComponents, res);
const finalState = store.getState();
res.status(200);
res.end(`</div>
${chunkExtractor.getScriptTags()}
<script type="text/javascript">window.INITIAL_STATE = ${JSON.stringify(
finalState,
).replace(/</g, '\\u003c')}</script>
</div>
`
);

Basically:

  • renderReactApp is a function that does the React streaming portion.
  • We have another async function, not shown here, that grabs the indexHtml string parts we need to stream that React isn’t responsible for.
  • We render Helmet before we start streaming. More on this later.
  • We render all the JavaScript chunks that the user should download. More on this later.
  • We render the final state from our SSR Redux store for the user to pickup on.

Streaming definitely adds complexity, and this is where I started to get nervous — a lot of code was piling up. However, the reason we were adding streaming was for the stability and the quality of the application — not for performance. It is important to keep in mind why so we don’t get carried away pre-optimizing.

React Helmet Async

Using React Helmet with SSR usually involves rendering Helmet in the <head> after renderToString is called. The way React Helmet is designed, you have to have already rendered the html. A very helpful developer has solved this problem with react-helmet-async, which also includes how to use it with streaming.

Side note: In order to use react-helmet-async, you still need to “walk the react tree” in order to visit all the child components that might be using React Helmet. The way we accomplished this was with react-tree-walker.

Styled Components Streamed

Styled Components usually renders all of its CSS in one spot, but when you are streaming, it needs to render the CSS as React is streaming. Styled Components has a guide on how to do stream interleaving. This is super cool stuff — the server will stream CSS as it renders the components. The browser can start rendering that component, with the CSS it immediately needs. This is a great user experience.

Webchunks

We already had our webpack build splitting our JavaScript into chunks using loadable-components.

loadable-components has an article on how to make sure that the proper chunks are defined in the HTML for the page the user is requesting.

Performance

Although we were targeting for minimum viable product, we had some concerns with adding SSR. Will it be able to handle the traffic? Will it slow down the user experience too much? If you do not optimize at all, you could be looking at adding several seconds to a user’s initial request and a heavy increase in CPU load on your web servers.

Caching API Calls

This is easy enough to do and should be done anyway. We internally cached all the API calls that SSR would be making, ensuring our APIs respond speedily. We used Nginx for an HTTP cache.

API Calls should be made inside the cluster

API calls inside the cluster are significantly faster. We used our internal DNS entries on API calls on SSR.

Caching SSR

This is probably the most important thing to do for SSR performance. Cache the HTTP output! You do not want to be rendering your homepage hundreds of times a minute. You only need to render it once. We did this the same way we cached our API calls — via Nginx. We plan on following up with utilizing caching at an edge server using our CDN.

Optimizing your React application

We had some CPU bottlenecks in our React application that were not apparent until we started using SSR. They were issues on the client-side, but we had awesome developer machines and didn’t notice. Once we added SSR, with web servers hosting it with a fraction of the CPU power of our machines, we saw 2–5 second load times. It turns out, we were using Immutable.js incorrectly — we had to refactor our application to improve the performance.

Visual artifacts

When we used SSR, we noticed the site would “flicker” when the browser would render the SSR content before it rendered CSS, fonts, etc. We had to do several days worth of development to refactor away from global CSS. We had to rewrite some of our media queries to be more friendly for SSR. Luckily, this was something we could work on in parallel while other SSR improvements were made. A developer working on visual artifacts does not have to get too deep in the weeds on how SSR works.

This seems like too much. How can I be more iterative? How can we reduce the risk?

We added a query string parameter to toggle off SSR on an individual request and a feature toggle to toggle it off completely. This way, we could turn on SSR in dev — but leave it off for our other environments. When we launched it in prod, we could disable it on any individual request to make sure a bug wasn’t related to SSR.

Feature toggles reduce a lot of the risk and help make sure you can integrate your code into the main development branch frequently. Launching SSR dark will help you stay iterative even though it’s a wide-sweeping change across the application.

Here is what that feature toggle looks like:

if (process.env.DISABLE_SSR) {
app.get('*', serveApp);
} else {
app.get('*', serveSsrApp);
}

Our serveApp function serves the index.html the “old way” that most React tutorials have you set up: an express server that serves the webpack built index.html file.

What next?

SSR and Hot reloading

Server side rendering added a lot of complexity. We still have problems during local development with utilizing features that the Node.js server will not have on it. We want to make sure all local development is done on a server using SSR. Right now, the way we have it configured, you can either get the nice local development hot-reloading experience OR you can get SSR. We want to have our cake and eat it too!

CDNs and aggressive caching

Our caching expires quickly and is not available on edge servers. To improve performance and load, we need to be better about caching content that does not change very often.

More improvements to the client side

We’ve improved our application a lot, but we still haven’t measured our bottlenecks on the client side application. We still have noticed a flicker where we believe React is re-rendering the entire HTML. Since our goal was not performance related, we have been putting this off, but we will be addressing that in the future for the best user experience!

Cool. So how did it do?

Aside from Facebook sharing and SEO, we saw a huge improvement in performance:

We still have a long way to go to make our app performant, but that comes with the territory of aggressive MVP. We’re confident Spreetail.com can become a leader on the end user’s experience in ecommerce!

Spreetail Engineering

Insights and discussions from the team building the best online shopping experience to date.

Stephen England

Written by

Spreetail Engineering

Insights and discussions from the team building the best online shopping experience to date.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade