What’s New With Server-Side Rendering in React 16

74,673 reads

A quick introduction to the new features in React 16 SSR, including arrays, performance, streaming, and more.

React 16 is here! 🎉🎉🎉

There are lots of exciting new bits (most notably the Fiber rewrite), but personally, I’m most excited about React 16's many improvements that have been made to server-side rendering.

Let’s take a deep dive into what’s new and different with SSR in React 16, and I hope you’ll end up as excited as I am!

How SSR Works In React 15

First of all, let’s get a refresher on what server-side rendering looks like in React 15. To do SSR, you generally run a Node-based web server like Express, Hapi, or Koa, and you call renderToString to render your root component to a string, which you then write to a response:

// using Express

import { renderToString } from "react-dom/server"

import MyPage from "./MyPage"

app.get("/", (req, res) => {

res.write("<!DOCTYPE html><html><head><title>My Page</title></head><body>");

res.write("<div id='content'>");

res.write(renderToString(<MyPage/>));

res.write("</div></body></html>");

res.end();

});

Then, in your client bootstrap code, you tell the client-side renderer to “rehydrate” the server-generated HTML using render() , the same method you would use in a client-side rendered app:

import { render } from "react-dom"

import MyPage from "./MyPage"

render(<MyPage/>, document.getElementById("content"));

If you do this correctly, the client-side renderer can just use the existing server-generated HTML without updating the DOM.

So, what does SSR look like in React 16?

React 16 Is Backwards Compatible

The React team has shown a strong commitment to backwards compatibility, so if you were able to run your code in React 15 without any deprecation warnings, it should “just work” in React 16. The code in the section above, for example, works just fine in both React 15 and React 16.

If by chance you drop React 16 into your app and do find errors, please file them! That will help the core team mop up any bugs with the 16 release.

render() Becomes hydrate()

When you upgrade SSR code from React 15 to React 16, though, you will probably come across the following warning in your browser:

Yet another helpful React warning. render() is now hydrate()!

It turns out that in React 16, there are now two different methods for rendering on the client side: render() for when you are rendering the content solely on the client side, and hydrate() for when you are rendering on top of server-side rendered markup. Because React is backwards compatible, render() will continue to work when rendering on top of server-generated markup in React 16, but you should change those calls to hydrate() to quiet the warning and prepare your code for React 17. The code snippet we showed above would change to:

import { hydrate } from "react-dom"

import MyPage from "./MyPage"

hydrate(<MyPage/>, document.getElementById("content"));

React 16 Can Deal With Arrays, Strings, and Numbers

In React 15, a component’s render method must always return a single React element. In React 16, though, the client-side renderer allows components to also return a string, a number, or an array of elements from a render method. Naturally, this feature is also supported by React 16’s server-side rendering.

So, you can now server-render components that look like this:

class MyArrayComponent extends React.Component {

render() {

return [

<div key="1">first element</div>,

<div key="2">second element</div>

];

}

}

class MyStringComponent extends React.Component {

render() {

return "hey there";

}

}

class MyNumberComponent extends React.Component {

render() {

return 2;

}

}

You can even pass in a string, a number, or an array of components to the top-level renderToString method:

res.write(renderToString([

<div key="1">first element</div>,

<div key="2">second element</div>

]));

// it’s not entirely clear why you would do this, but it works!

res.write(renderToString("hey there"));

res.write(renderToString(2));

This should let you eliminate any div s and span s that were just added for your React component tree, leading to an overall smaller HTML document size.

React 16 Generates More Efficient HTML

Speaking of a smaller HTML document size, React 16 also radically cuts down the SSR overhead in generated HTML. In React 15, each HTML element in an SSR document has a data-reactid attribute, whose value consists of a monotonically increasing ID, and text nodes are sometimes surrounded by comments with react-text and an ID. To see what this would look like, take the following snippet of code:

renderToString(

<div>

This is some <span>server-generated</span> <span>HTML.</span>

</div>

);

In React 15, this snippet generates HTML that looks like this (with newlines added for readability):

<div data-reactroot="" data-reactid="1"

data-react-checksum="122239856">

<!-- react-text: 2 -->This is some <!-- /react-text -->

<span data-reactid="3">server-generated</span>

<!-- react-text: 4--> <!-- /react-text -->

<span data-reactid="5">HTML.</span>

</div>

In React 16, however, all of the IDs have been removed from the markup, so the HTML for that same snippet is considerably simpler:

<div data-reactroot="">

This is some <span>server-generated</span> <span>HTML.</span>

</div>

Not only is this much cleaner to read, it can reduce the size of the HTML document considerably. Yay!

React 16 Allows Non-Standard DOM Attributes

In React 15, the DOM renderer was fairly strict about attributes on HTML elements, and it stripped out any non-standard HTML attributes. In React 16, though, both the client and server renderer now pass through any non-standard attributes that you add to HTML elements. To learn more about this feature, read Dan Abramov’s post on the React blog about the change.

React 16 SSR Doesn’t Support Error Boundaries or Portals

There are two new features in the React 16 client-side renderer that are unfortunately not supported in the server-side renderer: Error Boundaries and Portals. If you want to learn more about error boundaries, check out Dan Abramov’s excellent post on the React blog, but know that (at least for now) error boundaries do not catch errors on the server. Portals don’t yet have an explanatory blog post as far as I know, but the Portal API requires a DOM node, so it can’t be used on the server.

React 16 Performs Less Strict Client-Side Checking

When you rehydrate markup on the client-side in React 15, ReactDOM.render() performs a character-for-character comparison with the server-generated markup. If for any reason there’s a mismatch, React raises a warning in development mode and replaces the entire tree of server-generated markup with HTML that has been generated on the client.

In React 16, though, the client-side renderer uses a different algorithm to check that the server-generated markup is correct. It’s a bit more lenient than React 15; for example, it doesn’t require that the server-generated markup have attributes in the same order as they would be in on the client side. And when the client-side renderer in React 16 detects a markup mismatch, it only attempts to change the HTML subtree that doesn’t match, rather than the entire HTML tree.

Generally, this change shouldn’t have much effect for end users, except for one fact: React 16 doesn’t fix mismatched SSR-generated HTML attributes when you call ReactDOM.render()/hydrate() . This performance optimization means that you will need to make extra sure that you fix any markup mismatch warnings you see in your app in development mode.

React 16 Doesn’t Need To Be Compiled For Best Performance

In React 15, if you used SSR straight out of the box, performance was less than optimal, even in production mode. This is because there are a lot of great developer warnings and hints in React, and each of those warnings looks something like this:

if (process.env.NODE_ENV !== "production") {

// check some stuff and output great developer

// warnings here.

}

Unfortunately, it turns out that process.env is not a normal JavaScript object, and it is quite expensive to get a value out of it. So even when the value of NODE_ENV is set to production , just checking the environment variable so frequently adds a significant amount of time to server rendering.

To solve this problem in React 15, you have to compile your SSR code to remove references to process.env , using something like Webpack’s Environment Plugin, or Babel’s transform-inline-environment-variables plugin. In my experience, though, a lot of folks don’t compile their server-side code, and they get significantly worse SSR performance as a result.

In React 16, this problem has been solved. There is only one call to check process.env.NODE_ENV at the very beginning of React 16, so there’s no need to compile your SSR code for best performance. You get best performance right out of the box.

React 16 Is Also Just Faster

Speaking of performance, folks who use React server-side rendering in production often complain that large documents render slowly, even with every best practice in place*.

So, I’m very happy to report that some of the preliminary testing I’ve done shows dramatic speed ups in server-side rendering in React 16, across multiple different versions of Node:

React 16 renders on the server faster than React 15

When comparing against React 15 with process.env compiled out, there’s about a 2.4x improvement in Node 4, about a 3x performance improvement in Node 6, and a full 3.8x improvement in the new Node 8.4 release. And if you compare against React 15 without compilation, React 16 has a full order of magnitude gain in SSR in the latest version of Node!

Why is React 16 SSR so much faster than React 15? Well, in React 15, the server and client rendering paths were more or less the same code. This meant that all of the data structures needed to maintain a virtual DOM were being set up when server rendering, even though that vDOM was thrown away as soon as the call to renderToString returned. This meant there was a lot of wasted work on the server render path.

In React 16, though, the core team rewrote the server renderer from scratch, and it doesn’t do any vDOM work at all. This means it can be much, much faster.

Now, a caveat: the test I made just generates a giant tree of <span> s with one very simple recursive React component. This means that it is a very synthetic benchmark and almost certainly doesn’t reflect real-world use. If you have a bunch of complex render methods in your components that take up a lot of CPU cycles, for example, there’s nothing React 16 can do to make that faster. So while I absolutely expect to see React SSR times get significantly better with the move to 16, I don’t expect you will see a 3x improvement in your real world app. Anecdotally, I’ve heard from some early adopters that they are seeing about a 1.3x speedup. The best way to find out in your app is to test it out and see!

React 16 Supports Streaming

Last but certainly not least, React 16 now supports rendering directly to a Node stream.

Rendering to a stream can reduce the time to first byte (TTFB) for your content, sending the beginning of the document down the wire to the browser before the next part of the document has even been generated. All major browsers will start parsing and rendering the document earlier when the content is streamed from the server this way.

The other great thing that you get from rendering to a stream is the ability to respond to backpressure. In practical terms, this means that if the network is backed up and can’t accept more bytes, the renderer gets a signal and pauses rendering until the clog has cleared up. This means that your server uses less memory and is more responsive to I/O conditions, both of which can help your server stay up in challenging conditions.

To use React 16’s render to stream, you need to call one of two new methods on react-dom/server : renderToNodeStream or renderToStaticNodeStream , which correspond to renderToString and renderToStaticMarkup , respectively. Instead of returning a string, these new methods return a Readable , the Node Stream class used for objects that emit a stream of bytes.

When you receive the Readable stream back from renderTo(Static)NodeStream , it is in paused mode, and no rendering has happened yet. Rendering only starts if you call read or, more likely, pipe the Readable to a Writable stream. Most Node web frameworks have a response object that inherits from Writable , so you can generally just pipe the Readable to the response.

As an example, the Express example from above could be rewritten with streaming like this:

// using Express

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

import MyPage from "./MyPage"

app.get("/", (req, res) => {

res.write("<!DOCTYPE html><html><head><title>My Page</title></head><body>");

res.write("<div id='content'>");

const stream = renderToNodeStream(<MyPage/>);

stream.pipe(res, { end: false });

stream.on('end', () => {

res.write("</div></body></html>");

res.end();

});

});

Note that when we pipe to the response object, we have to include the optional argument { end: false } to tell the stream not to automatically end the response when the renderer finishes. This allows us to finish up the HTML body and end the response ourselves once the stream has been fully written to the response.

Streaming Has Some Gotchas

While rendering to a stream should be an upgrade in most scenarios, there are some current SSR patterns that don’t work well with streaming.

Generally, any pattern that uses the server render pass to generate markup that needs to be added to the document before the SSR-ed chunk will be fundamentally incompatible with streaming. Some examples of this are frameworks that dynamically determine which CSS to add to the page in a preceding <style> tag, or frameworks that add elements to the document <head> while rendering. If you use these kinds of frameworks, you’ll probably have to stick with string rendering.

Another pattern that does not yet work in React 16 is embedding calls to renderToNodeStream into component trees. In React 15, it is fairly typical to use renderToStaticMarkup to generate the page template and embed calls to renderToString to generate dynamic content, like so:

res.write("<!DOCTYPE html>");

res.write(renderToStaticMarkup(

<html>

<head>

<title>My Page</title>

</head>

<body>

<div id="content">

{ renderToString(<MyPage/>) }

</div>

</body>

</html>);

If you replace these render calls with their streaming counterparts, though, this code will stop working, because it is not yet possible for a Readable stream (which is returned from renderToNodeStream ) to be embedded as an element in a component. I’m hoping this gets added in later, however!

That’s It!

So those are the major SSR changes in React 16; I hope you are as excited about them as I am.

Before I wrap up, I want to give my heartfelt thanks to all the members of the React core team who have worked on making server-side rendering a first class part of the React ecosystem. This group includes (but is by no means limited to) Jim Sproch, Sophie Alpert, Tom Occhino, Sebastian Markbåge, Dan Abramov, and Dominic Gannaway. Thank you, thank you, thank you!

Now: let’s get out there and server render some HTML!

Thanks to Sunil Pai, Dan Abramov, Alec Flett, Swarup Karavadi, Helen Weng, and Dan Fabulich for reviewing this article.

P.S. I spoke at React Boston on September 23rd about these topics, along with some musings about where we could go next with server-side rendering post-React 16. If you’d like to see that talk, it’s in the archived event livestream here. If you’d like to skip the content that’s in this article and watch just the ideas for the future of SSR, click here.

*Speaking of which: please, please, please make sure you always set NODE_ENV to production when using React SSR in production!

Tags