React’s most highly touted feature in the early days was the ability to build “universal” apps - apps that could run in the browser and render HTML on the server. Server-side rendering (SSR) would help ensure that search engine crawlers index your site properly and provide a 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 most 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 counter-productive. It leads to huge infrastructure costs and 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. 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 unaware that it’s a necessity for their apps. Many of those aware of its 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.
Over the last few years, several “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 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 the 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 developing, optimizing, and maintaining your website.
In addition to these concerns, there are at least two major drawbacks to pre-rendering:
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 several 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?
Pre-rendering services don’t help generate 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 are required to achieve the best possible page load speeds.
Pre-rendering seems to be a band-aid: 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.
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 the 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 challenging to get right.
URLs represent the 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 backward.
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 the node and return a 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 that can be called in node.js during SSR. So, this:
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! </route>
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. You should implement code splitting to keep your app as fast as possible.
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 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), improving your website speed and search engine ranking.
Lazy loading presents a unique challenge when implementing SSR. The server knows which components it uses 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 and 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 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:</script></loadable.capture>
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.
When writing a universal PWA, you must 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.
Layer0's open-source React PWA framework, React Storefront, does SSR correctly. Applications built with React Storefront are universal by default. Furthermore, React Storefront leverages SSR to add some pretty amazing capabilities to your PWA.
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:
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 take advantage of caching. Furthermore, React Storefront’s router properly decouples state management from components. Routes determine the state, and the state determines which components are displayed.
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 give you a convenient place to declare skeletons to display while pages are loading.
One huge benefit unique to React Storefront is its ability to provide AMP equivalents for every page in your PWA automatically. 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 different from a PWA and 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.
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 Layer0, 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 supporting SSR is easy if you use the right React framework and start building with SSR in mind from day one.
Get the information you need. When you’re ready, chat with us, get an assessment or start your free trial.