One of React’s most highly touted features in the early days was the ability to build “universal” apps - apps that could not only run in the browser, but also render HTML on the server.  Server-side rendering (SSR) would help ensure that search engine crawlers index your site properly and provide a really good time to first paint (TTFP). Now, six years into React’s life span, SSR doesn’t get much press. Why is that?

All sorts of different apps are built on React. For some, SSR just isn’t that important. Any app where the majority of the content depends on the state of the signed in user’s account isn’t likely to benefit much from SSR. Expensify and Google Calendar are good examples. Neither would benefit from SSR because most of their content (a) isn’t publicly available and therefore can’t be indexed by search engines, and (b) is unique per user, so it isn’t cacheable. Using server-side rendering for non-cacheable content is actually counter-productive. It leads to huge infrastructure costs and actually provides a slower user experience than simply rendering on the client after loading a lightweight app shell.

But, if you try to launch an eCommerce site without server-side rendering, you’re crazy. It’s absolutely essential. You can’t risk your SEO, not to mention that every millisecond improvement in real and perceived performance translates directly into revenue. Furthermore, your app is probably highly cacheable. Sure, you may have some personalization on your pages, but you can late-load that. I bet 80% or more of the content on every page is the same for every user. Shouldn’t that be displayed to the user as quickly as possible?

Sadly, the ever broadening usage of React frameworks has diluted the perceived value of SSR. Many of the developers I’ve talked to in eCommerce are totally unaware that it’s a necessity for their apps. Many of those that are aware of it’s importance aren’t well versed in how it differs from pre-rendering, the challenges of implementing proper SSR, and how to make the most of SSR given the unique requirements of an eCommerce website.

Pre-rendering != SSR

Over the last few years, a number of “pre-rendering” solutions have sprung up. Libraries like rendertron and puppeteer, and services like prerender.io give you the tools you need to render your app on the server. These work great, at least in theory, for delivering pre-rendered HTML content to crawlers, assuming:

  • You can properly detect all crawlers (and you keep that logic up to date).
  • You feel comfortable maintaining the infrastructure required to keep a pre-rendering service up and running (not a concern for prerender.io, since they maintain the infrastructure and simply charge by the page)
  • You’re willing to put up with some lag time between new content becoming available and when the pre-render cache is updated, or you are planning to put in place mechanisms for re-rendering when pages change. Do you know how to put hooks in your CMS so that you can clear your pre-render cache whenever content is updated? How about watching your store’s back-end for pricing changes and new inventory?

Even if none of the above are deal-breakers for you and your team, no pre-rendering solution is trivial to implement. You’ll need to manage new infrastructure to run Headless Chrome, configure your CDN to properly cache pre-rendered pages, and route bots to your pre-rendering service (at the very least). This represents additional work you’ll need to do on top of developing, optimizing, and maintaining your website.

In addition to these concerns there at least two major drawbacks to pre-rendering:

Real users need not apply

Pre-rendering can’t be used for delivering content to actual users. It’s only useful for bots and crawlers. Try to hydrate a React progressive web app on the client over a pre-rendered HTML body. You’ll undoubtedly run into a number of issues. You won’t be able to use code-splitting.  Your CSS styles probably won’t work, and you’ll likely experience some content-flash when the app mounts. And, if your app wasn’t written with server-side rendering in mind, you’ll probably have deeper issues with state management when initializing on the client. For example, how will you populate your store with the correct initial state so that client-side hydration yields the same HTML output as the server-side render?

No help with AMP

Pre-rendering services don’t offer any help with generating AMP content for your web app. This is something that (spoiler alert) the React Storefront framework can do automatically. This is a huge opportunity missed. You’ll need to implement your site once as a PWA and a second time as AMP. These technologies have nothing in common, but both are required to achieve the best possible page load speeds.

Pre-rendering seems to be a bandaid: A solution for developers who didn’t realize they needed SSR when they started building their app and want a temporary way to salvage their work before moving on to a better solution.

SSR from the Start

If you’re building an eCommerce progressive web app, do yourself a big favor and choose an architecture that supports SSR from day one. SSR is a hard requirement because it enables proper SEO and improves website speed, which increases conversion rates.  

If you’re not familiar with how SSR works, here’s a basic overview of the request flow:

Browser => request => node.js => initial app state => React => HTML => response => initial paint => ReactDOM.hydrate().

Normally, React code running in the browser converts application state into HTML. When an app supports SSR, the same code also runs on the server, with a few restrictions, and the resulting HTML is served to the browser. Those restrictions, along with the need for additional infrastructure, and the fact that most articles, examples, and tutorials written by the React community completely ignore SSR make SSR surprisingly difficult to get right.

Why SSR is difficult

React Router

URLs represent application state. For example, a URL of /products/red-shirt represents a state in which the Product view displays the information about a red-shirt. Every user who clicks on a link to /products/red-shirt should see the same app in the same state (perhaps with a bit of personalization mixed in). The URL determines the route, the route determines the state, and the state determines which components are displayed. Nearly every React app uses react-router, which gets this backwards.  

With React Router, the route determines the components that are rendered. The component is entrusted with fetching the correct state. Most tutorials have you burying the asynchronous code that fetches state from your API in a React component lifecycle method, like componentDidMount, which only runs on the client. If you’re going to use React Router for server-side rendering, you’ll need to hoist that logic out of the component, so it can be run in node and return state that can be passed to ReactDOMServer.renderToString().  

If you search for “react-router SSR” on Google, you’ll get a link to this article, which recommends pulling your top level routes out of your React components into an array of objects, each defining a loadData method which can be called in node.js during SSR. So, this:

import App from './App';
import Home from './Home';
import Posts from './Posts';
import Todos from './Todos';
import NotFound from './NotFound';

import loadData from './helpers/loadData';

const Routes = [
  {
    path: '/',
    exact: true,
    component: Home
  },
  {
    path: '/posts',
    component: Posts,
    loadData: () => loadData('posts')
  },
  {
    path: '/todos',
    component: Todos,
    loadData: () => loadData('todos')
  },
  {
    component: NotFound
  }
];

This works, but it certainly doesn’t use React Router as intended. Now you’ll have at least two different ways for declaring routes, perhaps even three: in Express, an array of top level routes, and further nested <Route> elements within your components that are only meant to run on the client. What a mess!  

Lazy loading and code splitting

Your app will grow over time as you continue to add features. Many applications have complex features that are rarely used. The code for those features will unnecessarily slow down your app. To keep your app as fast as possible, you should implement code splitting.  

Typically, this means creating separate bundles for each top-level component in your app.  You’ll have separate bundles for the home page, product listings, products, etc… That way, when a user arrives at your app via a link to a specific product, the browser doesn’t need to download and run the code for all of the other pages before the app becomes interactive. The app can “lazy-load” other page components if and when the user ever navigates to them. This saves bandwidth and decreases first input delay or FID (note FID is often approximated by time to interactive or TTI metric), which improves both your website speed and search engine ranking.

Lazy loading presents a unique challenge when implementing SSR. The server knows which components it used to render the outgoing HTML. It should send the code for those components alongside the HTML. Otherwise, the app will need to mount in the browser and run two render cycles before it’s ready to be interactive, which will increase FID (and TTI), and likely cause some content flash.  

Lazy loading an SSR are highly interdependent. The library you choose for lazy loading will affect the way you generate the final HTML that’s sent back in the response. React Loadable is a popular choice. To capture the bundles that need to be loaded in order to hydrate the HTML that was rendered on the server, you’ll need to add <Loadable.Capture> to your SSR code, then use React Loadable’s getBundles function to figure out which <script> tags need to be added to the document body. Here’s an example:

import Loadable from 'react-loadable';
import { getBundles } from 'react-loadable/webpack'
import stats from './dist/react-loadable.json';

app.get('/', (req, res) => {
  let modules = [];

  let html = ReactDOMServer.renderToString(
    <Loadable.Capture report={moduleName => modules.push(moduleName)}>
      <App/>
    </Loadable.Capture>
  );

  let bundles = getBundles(stats, modules);

  // ...

For this to work, you’ll also need to alter your webpack build to output a react-loadable.json stats file and add the react-loadable/babel plugin to your babel config.  SSR, lazy-loading, and the app build process all need to work in harmony.

Side effects

When writing a universal PWA, you need to be careful not to access browser APIs, such as the window and document objects, when running on the server. This means delaying side effects until the componentDidMount part of the react lifecycle. This is relatively simple, if you’ve written your app with SSR in mind from day one. If not, you’ll likely assume that browser APIs are always available and access to them will be sprinkled throughout your code base.  Switching to SSR later will be painful.

React Storefront makes SSR easy

Moovweb’s open-source React PWA framework, React Storefront, does SSR the right way. Applications built with React Storefront are universal by default. Furthermore, React Storefront leverages SSR to add some pretty amazing capabilities to your PWA.

Isomorphic routing done right, done once

React Storefront provides its own router that allows you to declare routes in exactly one place.  From the back end, to the CDN, to the front-end, routes are declared in a single JS API:

import { Router, fromClient, fromServer, cache } from 'react-storefront/router'

new Router()
  .get('/products/:id', 
    cache({ 
      server: { maxAgeSeconds: 300 } // cache the result for 5 minutes on the server 
      client: true // cache on the client using the service worker as well
    }),

    // Display the product view.  If there is a custom skeleton configured for products, it will be displayed immediately while product data is fetched from the server.
    fromClient({ page: 'Product' }),

    // Fetch the product data from the server
    fromServer('./product/product-handler')
  )

In addition to keeping your code DRY, React Storefront’s router allows you to attach both front and back-end caching logic to each route. Caching is a huge factor in improving website speed.  Making it convenient to configure is the only way to ensure that developers will actually take advantage of caching. Furthermore, React Storefront’s router properly decouples state management from components. Routes determine state and state determines which component are displayed.

Lazy loading made easy

React Storefront automatically configures webpack, babel, and react-universal-component to give you code splitting and lazy loading out of the box. Your PWA is automatically split into separate bundles for each page component, and a single unified bundle is used when rendering on the server. Simply use the Pages component to register each lazy-loaded component. Pages also gives you a convenient place to declare skeletons to display while pages are loading.

import React, { Component } from 'react'
import Pages from 'react-storefront/Pages'
import ProductSkeleton from './product/ProductSkeleton'

function App() {
  return (
    // Switches the active page based on the "page" field in the application state, which is set by the router in the previous example.
    // Also caches previously viewed pages in the DOM so going back is instant
    <Pages
      loadMasks={{
        Product: ProductSkeleton // a placeholder component to display as product data is loading in and/or when the Product component is being lazy-loaded
      }}
      components={universal => ({
        Home: universal(import("./home/Home")), // lazy load the Home component
        Product: universal(import("./product/Product")) // lazy load the Product component
      })}
    />
  )
}

Leveraging SSR to get AMP support for free

One huge benefit which is unique to React Storefront is its ability to automatically provide AMP equivalents of every page in your PWA. AMP is essentially a subset of HTML that, if you adhere to, Google will preload your AMP app from their CDN into the user’s browser whenever it shows up in a search result. This makes the transition to your app near-instantaneous. The downside is it’s totally different from a PWA and basically means writing your app twice… unless you use the React Storefront framework to build your eCommerce PWA.

React Storefront uses a combination of AMP-aware components and post-SSR transformations to automatically convert the PWA’s HTML to valid AMPHTML, whenever the URL ends in “.amp”. This topic was covered in depth in our previous post. Suffice it to say that this would not be possible without SSR.

AMP doesn’t even allow you to use JavaScript, so it’s impossible to render AMP from React on the client. It must be done on the server. Even with SSR in place, there are a lot of hoops to jump through to convert a React progressive web app into a valid AMP app. Fortunately, React Storefront handles all of that for you.

The takeaway

Too many developers in eCommerce aren’t aware that:

  • SSR is a must have for all eCommerce websites. Your SEO, SEM, and conversion rate depend on it.
  • Almost all eCommerce apps are highly cacheable. With SSR running on the right infrastructure, like the Moovweb XDN, you can leverage this to provide sub-second page loads and instant websites.
  • Pre-rendering is not the same as SSR. It might help your SEO, but it won’t help you improve page load times.
  • Building a PWA that supports SSR is easy, if you use the right React framework, and you start building with SSR in mind from day one.