Innovations in server rendering in React 16

Original author: Sasha Aickin
  • Transfer
React 16 released! Talking about this event, I can mention a lot of great news (like the architecture of the Fibers kernel), but personally I am most admired about the improvements in server rendering. I propose to analyze and compare all this in detail with what it was before. I hope you enjoy server-side rendering in React 16 the way I liked it.



How SSR works in React 15


First, let's recall how Server-Side Rendering (SSR) looks in React 15. For SSRs, they usually support a Node-based server using Express, Hapi, or Koa and invoke it renderToStringto convert the root component to a string, which is then written server response:

// используем Express
import { renderToString } from "react-dom/server"
import MyPage from "./MyPage"
app.get("/", (req, res) => {
  res.write("My Page");
  res.write("
");    res.write(renderToString());  res.write("
");  res.end(); });

When the client receives a response, the client rendering subsystem, in the template code, is given the command to restore the HTML generated on the server using the method render(). The same method is used in applications that render on the client without server involvement:

import { render } from "react-dom"
import MyPage from "./MyPage"
render(, document.getElementById("content"));

If done correctly, the client rendering system can simply use the HTML generated on the server without updating the DOM.

What does SSR look like in React 16?

React 16 Backward Compatibility


The React development team showed a clear focus on backward compatibility. Therefore, if your code runs in React 15 without reporting obsolete constructs, it should just work in React 16 without any extra effort on your part. The code above, for example, works fine in both React 15 and React 16.

If it happens that you run your application on React 16 and encounter errors, please report them! This will help the development team.

The render () method becomes the hydrate () method


It should be noted that when switching from React 15 to React 16, you may come across the following warning in the browser.


Another useful React warning. The render () method is now called hydrate ()

It turns out that in React 16 there are now two different methods for rendering on the client side. A method render()for situations where rendering is performed entirely on the client, and a method hydrate()for cases where rendering on the client is based on the results of server rendering. Due to the backward compatibility of the new version of React, it render()will work even if you pass to it what came from the server. However, these calls should be replaced by calls.hydrate() so that the system stops issuing warnings, and in order to prepare the code for React 17. With this approach, the code shown above would change like this:

import { hydrate } from "react-dom"
import MyPage from "./MyPage"
hydrate(, document.getElementById("content"))


React 16 can work with arrays, strings and numbers.


In React 15, a component method render()should always return a single React element. However, in React 16, client-side rendering allows components to also return a render()string, number, or array of elements from a method . Naturally, this also applies to SSR.

So, now you can perform server-side component rendering, which looks something like this:

class MyArrayComponent extends React.Component {
  render() {
    return [
      
first element
,      
second element
   ];  } } class MyStringComponent extends React.Component {  render() {    return "hey there";  } } class MyNumberComponent extends React.Component {  render() {    return 2;  } }

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

res.write(renderToString([
      
first element
,      
second element
   ])); // Не вполне ясно, зачем так делать, но это работает! res.write(renderToString("hey there")); res.write(renderToString(2));

This should allow you to get rid of any divand spanthat simply added to your React component tree, which leads to a general reduction in the size of HTML documents.

React 16 Generates More Effective HTML


If we talk about reducing the size of HTML documents, then React 16, in addition, radically reduces the excessive load created by the SSR when generating HTML code. In React 15, every HTML element in an SSR document has an attribute data-reactidwhose value is a monotonically increasing ID, and text nodes are sometimes surrounded by comments with react-textand IDs. In order to see this, consider the following code fragment:

renderToString(
  
   This is some server-generatedHTML.  
);

In React 15, this snippet will generate HTML code that looks like the one shown below (line feeds added to improve code readability):

 This is some  server-generated    HTML.

In React 16, however, all IDs are removed from the markup; as a result, HTML obtained from the same code fragment will be much simpler:

 This is some server-generatedHTML.

This approach, in addition to improving the readability of the code, can significantly reduce the size of HTML documents. This is just great!

React 16 supports custom DOM attributes


In React 15, the DOM rendering system was quite limited in terms of attributes of HTML elements. She cleaned up non-standard HTML attributes. In React 16, however, both the client and server rendering systems now skip arbitrary attributes added to HTML elements. To learn more about this innovation, read Dan Abramov 's React blog post.

SSR in React 16 does not support error handlers and portals


There are two new features in the React client rendering system, which, unfortunately, are not supported in the SSR. These are Error Boundaries and Portals. An excellent post by Dan Abramov on the React blog is dedicated to error handlers . However, keep in mind that (at least for now) handlers do not respond to server errors. For portals, as far as I know, there is not even an explanatory article yet, but the Portal API requires a DOM node, as a result, it cannot be used on the server.

React 16 performs less stringent client-side validation


When you restore client-side markup in React 15, the call ReactDom.render()performs a character-by-character comparison with server-side markup. If, for any reason, a mismatch is detected, React issues a warning in design mode and replaces the entire markup tree generated on the server with the HTML that was generated on the client.

In React 16, however, the client rendering system uses a different algorithm to verify that the markup that came from the server is correct. This system, in comparison with React 15, is more flexible. For example, it does not require that the markup created on the server contain attributes in the same order in which they would be located on the client side. And when the client rendering system in React 16 discovers discrepancies, it only tries to change the distinct HTML subtree, instead of the entire HTML tree.

In general, this change should not particularly affect end users, except for one fact: React 16, when calledReactDom.render() / hydrate(), does not fix the mismatched HTML attributes generated by the SSR. This performance optimization means that you will need to be more careful about fixing markup mismatches that result in warnings that you see in mode development.

React 16 does not need to be compiled to improve performance


In React 15, if you use the SSR in the form in which it appears immediately after installation, the performance is far from optimal, even in mode production. This is because React has a lot of great warnings and tips for the developer. Each of these warnings looks something like this:

if (process.env.NODE_ENV !== "production") {
  // что-то тут проверить и выдать полезное
  // предупреждение для разработчика.
}

Unfortunately, it turns out that process.envthis is not an ordinary JavaScript object, and accessing it is a costly operation. As a result, even if the value is NODE_ENVset to production, frequent checking of the environment variable significantly slows down the server rendering.

In order to solve this problem in React 15, you would need to compile the SSR code to remove links to process.env, using something like the Environment Plugin in Webpack, or the transform-inline-environment-variables plugin for Babel. I know from experience that many do not compile their server code, which, as a result, significantly degrades the performance of SSR.

React 16 resolved this issue. There is only one call to checkprocess.env.NODE_ENVat the very beginning of React 16 code, you no longer need to compile SSR code to improve performance. Immediately after installation, without additional manipulations, we get excellent performance.

React 16 delivers better performance.


If we continue the discussion about productivity, we can say that those who used React server rendering in production often complained that large documents are processed slowly, even with all recommendations for improving performance. Here I want to note that it is recommended to always check that the variable is NODE_ENVset to a value productionwhen you use SSR in production.

I am pleased to report that, after conducting some preliminary tests , I found a significant increase in the performance of server rendering of React 16 on various versions of Node:


Rendering on a server in React 16 is faster than in React 15. The lower the bar, the better the result

When compared with React 16, even taking into account that React 15 calls process.envwere eliminated due to compilation, there is a performance increase of about 2.4 times in Node 4, 3 times - in Node 6, and a remarkable increase of 3.8 times in Node 8.4 . If you compare React 16 and React 15 without compiling the latter, the results on the latest version of Node will be amazing.

Why is React 16 so much faster than React 15? So, in React 15, the server and client rendering subsystems were, in general terms, the same code. This means the need to support the virtual DOM during server rendering, even though this vDOM was dropped as soon as the call returned renderToString. As a result, a lot of unnecessary work was done on the server.

In React 16, however, the development team rewrote server rendering from scratch, and now it is completely independent of vDOM. This gives a significant increase in productivity.

Here I would like to make one warning regarding the expected performance growth of real projects after the transition to React 16. My tests consisted in creating a huge tree from one very simple recursive component of React. This means that my benchmark belongs to the category of synthetic ones and almost certainly does not reflect the real use cases of React. If your components have many complex methodsrender, which takes many processor cycles, React 16 cannot do anything to speed them up. Therefore, although I expect to see accelerated server rendering when switching to React 16, I do not expect, say, a threefold increase in performance in real applications. According to unverified data, when using React 16 in a real project, it was possible to achieve a productivity increase of about 1.3 times . The best way to understand how React 16 will affect the performance of your application is to try it yourself.

React 16 supports streaming


The last of the new React features that I want to talk about is no less interesting than the rest. This is rendering directly to Node streams.

Stream rendering can reduce the time it takes to get the first byte (TTFB, Time To First Byte). The beginning of the document enters the browser even before creating the continuation of the document. As a result, all leading browsers will begin to parse and render the document faster.
Another great thing that can be obtained from rendering into a stream is the ability to respond to a situation where the server issues data faster than the network can receive it. In practice, this means that if the network is overloaded and cannot receive data, the rendering system will receive an appropriate signal and stop processing data until the network load drops. As a result, it turns out that the server will use less memory and be able to respond more quickly to I / O events. Both that and another are capable to help the server to work normally in difficult conditions.

In order to organize streaming rendering, you need to call one of two new methods react-dom/server: renderToNodeStreamor renderToStaticNodeStream, which correspond to the renderToStringand methods renderToStaticMarkup. Instead of returning a string, these methods return an objectReadable . Such objects are used in the Node flow model for entities that generate data.

When you receive a stream Readablefrom methods renderToNodeStreamor renderToStaticNodeStream, it is in the pause mode, that is, rendering at this point has not yet begun. Rendering will only begin if you call read , or, more likely, connect the stream Readableusing pipe to the Writable stream . Most Node web frameworks have a response object that is inherited from Writable, so you can usually just redirect Readableto the response.

Say the above Express example could be rewritten for streaming rendering as follows:

// используем Express
import { renderToNodeStream } from "react-dom/server"
import MyPage from "./MyPage"
app.get("/", (req, res) => {
  res.write("My Page");
  res.write("
");  const stream = renderToNodeStream();  stream.pipe(res, { end: false });  stream.on('end', () => {    res.write("
");    res.end();  }); });

Please note that when we redirect the stream to the response object, we need to use an optional argument { end: false }to tell the stream that it should not automatically complete the response when rendering is complete. This allows us to finish the body of the HTML document, and as soon as the stream is completely recorded in the response, complete the answer ourselves.

Pitfalls of streaming rendering


Stream rendering can improve many SSR scenarios, however, there are some patterns that stream data will not benefit.

In general, any template in which, based on the markup created during the server rendering, the data that must be added to the document before this markup is formed, will be fundamentally incompatible with streaming data. Among examples of this are frameworks that dynamically determine which CSS rules should be added to a page in the previous generated markup tag

Also popular now: