React suspense and server rendering

The recently revealed React suspense functionality has the potential to solve a lot of the current pain points in server rendering. At the Swedish public service television company we are heavily invested in server rendering as a means to deliver our digital experiences to as many citizens as possible, so I thought I’d take some time to share a few thoughts on this, but first, some background.

A few days ago Dan Abramov delivered another one of his stunning talks, this time the topic was the future of React. If you are at all interested in React, you should really go watch it right now, I’ll wait.

(Oh, and while you are in video mode, you might wanna check out the latest talk I did at Stockholm ReactJS Meetup about the difference between initial state server rendering and full state server rendering and how to solve some of the challenges in the latter case.)

Dan’s talk is the perfect background for this blogpost which I have been meaning to write for some time now, so you really should watch it before reading this. Also, this post is long and kind of technical, so grab a coffee before venturing on.

In short, what Dan presented through fantastic demos was basically how React is tackling two things, CPU-bound performance (slow devices) and IO-bound performance (slow network). The solutions to both of these are based on the new asynchronous Fiber reconciler.

Since the reconciler can now pause, resume and abort rendering after it has started, it can prioritise UI updates based on importance. This means we can get a way more responsive UI even for complex applications on slow devices out of the box. This part has been known for quite a while now, but for anyone not yet convinced about the gains of this, just go watch those demos…

When it comes to IO-bound performance I have been speculating in private for a while that this new reconciler could also let us solve some of the current pain points in server rendering since it could pause rendering until some piece of asynchronous data has arrived and then resume it. The second part of Dan’s talk and the ”big reveal” was basically this for the client-side, but with a way nicer API than I could imagine. The core team has named this ”suspense” and what it means is that we can basically code our render-functions as if the data was already there and let React take care of the rest:

const articleFetcher = createFetcher(
  () => fetch('/news-api/articles')
);

function ArticleComponent() {
  // If data is not available on read
  // React will pause rendering and
  // start fetching the data and
  // render when the data is available
  let article = articleFetcher.read();
  return <div>{article.title}</div>;
}

This post will explore some of the current pain points in server rendering and why and how the asynchronous reconciler and React suspense could alleviate so many of them.

If you are short on time and also very familiar with the current state of fetching data when server rendering and the solutions in use today, you could skip to the section React suspense to the rescue?.

The current state of fetching data when server rendering

I’ll mainly focus on the issues around getting asynchronous data into the application here, for other challenges, see other posts or my previous talk. Reasoning about server rendering can be hard, so let’s first of all see if we can try to find a visualisation for what we are about to talk about.

First let us consider a simple case with no asynchronous data.

Synchronous process on the server

In this first case, the request comes in to a Node server, we do some synchronous setup, we run render, we do some work afterwards and we respond with the markup, simple. The important thing to note in this case is that the entire process, including React render, is synchronous.

I’ll omit Request, Render and Respond in the visualisations from now on. They correspond to the start, green stuff and end.

Asynchronous data with centralised routing

Let’s move on to something more interesting, the case where we need to incorporate some data from an external source into our server rendered response. A very popular way to go about this is to co-locate your data needs with a centralised routing config. This lets you match what root components will get rendered by the requested url and fetch all the data needed for those root-components and their children before rendering.

Fetching data using a centralised routing-solution on the server

Let’s take a look at the tradeoffs here:

Plus

  • Parallel fetching of multiple data
  • Single render pass
  • Also works for prefetching on the client, even with code-splitting
    (We can fetch both code and data dependencies at the same time)

Minus

  • Requires centralised routing config
  • Requires data dependencies to be tied to route root components
  • Requires some global state external to React in order to be able to dehydrate and then rehydrate data in the browser
  • Impossible to modularise components with asynchronous data needs so that they work with server-side rendering

When we look hard enough at what we are actually doing here, after a while we realise that we are actually building up knowledge about our application component structure outside of the normal React rendering by delegating this to a routing solution. This is one reason why React Router v3 and earlier got so complex, having to reimplement the React lifecycles, but it is also why the server-side migration path for React Router v4 is so cumbersome.

Next, let’s take a look at actually co-locating data needs with components instead of routes.

Asynchronous data with multi-pass rendering

A more recent approach to getting external data into React server-side is to co-locate data needs with components and trigger fetching of this data during the render process. When the data comes back, we trigger a new render with the new data in order to get our final markup. Note that if you have multiple nested components with data dependencies, you would have to render more than twice to get this to work.

For simplicities sake, let’s take a look at what a two pass rendering could look like:

Process for using multiple render-passes to fetch data on the server

During the first render, we trigger data fetching from two different siblings. We finish rendering an incomplete state and then wait for the data to come back. When it does, we do a final render with the correct data.

Let’s look at the tradeoffs:

Plus

  • Parallel fetching of multiple data, but only for sibling components
  • Does not require a centralised routing config
  • All knowledge about the application component structure is kept inside of React
  • Data dependencies can be co-located with any component

Minus

  • Multiple render passes – bad for performance
  • Requires some global state external to React in order to be able to dehydrate/rehydrate data to the browser
  • Does not work easily with prefetching and code splitting on the client
  • Impossible to modularise components with asynchronous data-needs so that they work with server-side rendering
  • Not really supported by React, leading to hacky implementations
  • Some solutions only support two render passes, leading to a tricky mental model where you have to reason about which components can have co-located data dependencies and which can not

I want to quickly expand on two of the above claims. First of all, co-locating data dependencies with components and still being able to code-split and load both code and data in parallel is actually possible, but only by having a build step that breaks out the data dependencies into the common chunk. Relay Modern does this.

Secondly, when I’m talking about hacky implementations, I don’t mean that they are inherently bad, just that they are not native to React. For example, the implementation in react-apollo is very smart, but what it really does is sort of reimplement its own React render in order to let their users co-locate data dependencies with their components.

A quick summary of the current challenges and tradeoffs

After looking at these two different approaches for fetching data on the server, we could ask ourselves, what criteria would a perfect solution fulfil?

  • Ability to co-locate routes and data dependencies with components
  • Parallel fetching of data
  • Single render to avoid wasted CPU cycles
  • No knowledge about application component structure necessary outside of the React render, to avoid complexity
  • No global state external to React (usually in the form of third party libraries) needed to dehydrate and rehydrate data between server and client (At least for simple cases)
  • Ability to modularise components that have external data dependencies so they work both on the client and the server out of the box

Let us next take a look at how an asynchronous reconciler could help deliver on these criteria.

React suspense to the rescue?

When we have an asynchronous reconciler that can pause and resume rendering a whole new approach opens up, as demoed on the client-side in Dan’s talk. If we translate this to the visualisations we have been using for the server-side, it would look like this:

Possible process for fetching data on the server with React suspense

(If you skipped to this part, blue is the Node process outside of React, green is React render and grey is network requests.)

This looks very similar to the last process we looked at, but the main difference is that there is only one asynchronous render pass here. When React has nothing more to render and is waiting for data to come in, it can suspend the rendering until that data comes in and then resume rendering from the point where it got suspended, instead of re-rendering the entire tree. In other words, no more wasted CPU-cycles.

Let us take a look at how well this would map to the criteria we came up with before:

  • Ability to co-locate routes and data dependencies with components
  • Single render
  • No knowledge about application component structure necessary outside of the React render
  • No global state external to React necessary for dehydration/rehydration
  • Ability to modularise components
  • Parallel fetching of data

All of these seem fulfilled! I’ll talk a bit about dehydration/rehydration in the next section.

When it comes to parallel fetching of data, this would work out of the box for sibling components. If you have multiple staggered components with data dependencies you would have to lift the nested data dependencies up to a higher level to achieve full parallelism. The same goes for loading code and data dependencies in parallel on the client, you would have to lift the data dependencies up to the same place where you code-split the component, same as with any code-splitting you do really.

The benefits of being able to have modularised components that take care of their own data fetching, and that work on both the client- and the server-side by just importing them, would be tremendous.

So what’s the catch?

This all almost seems to good to be true and in a way it is. The server renderer is currently a unique snowflake amongst all the React renderers in that it doesn’t actually use the asynchronous Fiber reconciler. The server renderer currently contains all the logic for traversing a React tree and converting it to html and where the reconciler normally takes care of things like calling to appropriate lifecycle hooks, the server renderer has implemented this separately. This is also the reason componentDidCatch does not currently work on the server, the error handling-framework that has been built into the reconciler simply does not exist on the server (yet).

So does this mean we will never have support for suspense on the server-side? No, just that it’s a lot of work to implement.

In practice there are a huge number of things to solve, but on a purely conceptual level I can see a couple of things that are prerequisites to making suspense work server-side.

Component error handling on the server

Behind the scenes React will throw a promise if the data does not exist in the cache yet. This basically works the same as throwing an error and using componentDidCatch to catch it, but it happens behind the scenes. Since this framework does not exist on the server yet, I’m guessing this would be a prerequisite to making fetchers work.

Data cache must be able to dehydrate/rehydrate

Another requirement for server-side fetchers to work is that you would have to be able to dehydrate/rehydrate the data cache React uses behind the scenes between the server and the client. Optimally you wouldn’t have to do this per fetcher, but could instead get all the data on a per request/render basis.

Note that this does not necessarily need to contain all the data that is supposed to be dehydrated/rehydrated, you could still use third party solutions on top of this. This is even quite likely as long as we have no way of dehydrating/rehydrating component state, another exciting prospect in the future of server rendering.

One interesting and great side effect of this is that since the data cache is supposed to be a specification that third party libraries like Apollo could extend, this could turn into an official ”React-way” of dehydrating and rehydrating data between server and client, helping ease of modularisation.

Async render-function

This one seems obvious, we would need something like a ReactDOM.renderAsync-function that returns a promise that fulfils with markup and possibly all cached data for dehydration when a full render pass has completed and no data fetching is pending.

Handle placeholders and loading-states differently on the server

On the client we will use placeholders and other techniques to optimise the user experience. The exact same behaviour would not make sense on the server, we probably want to be able to control behaviour and timeouts differently for that case. On the server we also need some way at the top level to access which fetchers timed out or got an error so we can decide wether to render what we have or an error page.

One cool fallback that would be possible to support with minimal configuration is to simply render the placeholder and try to refetch the data on the client when it fails on the server. This would let us not block the request for too long, while still having the option of for example having a longer timeout on the client.

Bonus: Code splitting

As a bonus, I’ll include code splitting here. This part isn’t strictly needed to solve all of the other good stuff, but seeing how this is a tricky topic when combined with server rendering I thought I’d share a few quick thoughts on how suspense could help. The reason this is tricky is that when we server render we need all the code to be available before we do the first ReactDOM.render (or really ReactDOM.hydrate nowadays) on the client, otherwise this render will overwrite markup we rendered on the server with empty markup. So what if this wasn’t the case? What if we could simply tell React not to commit the first hydrate to DOM before all code split fetchers has finished loading? I think this would be totally feasible with an asynchronous reconciler.

In closing

As I have hopefully convinced you, having a simple and standardised, but easily extendable, way to have co-located asynchronous data loading on both the client and the server in React would be HUGE. The fetcher concept that Dan showed off in his talk is a great improvement for a lot of use cases on the client, but when considering the potential for the server-side the benefits are even greater.

On the client, we have been able to trigger data loading from inside components through lifecycle hooks since way back when React got popular, but this simply has not been possible on the server. This has made us set up separate and parallel solutions when dealing with server-side rendering and solving it would remove a lot of the current pain points.

On a conceptual level, the effect would be that we could stop juggling two different mental models when we develop for the server-side, the normal one for what happens inside of React, and one for what happens outside of it often in the form of routing and data fetching. Routes as components would suddenly make a lot of sense for the server-side as well and the extra knowledge and setup you need to develop server rendered apps would be greatly lessened.

This blog post is my first attempt at helping out in some small way by making explicit the challenges that exists today, the benefits this would give us and some of the things that would need to be solved to make fetchers work server-side.

It is also a call to action, let’s help the team out and get this done!

 

 

(Oh, and I’d love to discuss this further, tweet me @EphemeralCircle or let’s grab a coffee at ReactEurope!)