61
votes

I've noticed that the reactDOM.renderToString() method starts to slow down significantly when rendering a large component tree on the server.

Background

A bit of background. The system is a fully isomorphic stack. The highest level App component renders templates, pages, dom elements, and more components. Looking in the react code, I found it renders ~1500 components (this is inclusive of any simple dom tag that gets treated as a simple component, <p>this is a react component</p>.

In development, rendering ~1500 components takes ~200-300ms. By removing some components I was able to get ~1200 components to render in ~175-225ms.

In production, renderToString on ~1500 components takes around ~50-200ms.

The time does appear to be linear. No one component is slow, rather it is the sum of many.

Problem

This creates some problems on the server. The lengthy method results in long server response times. The TTFB is a lot higher than it should be. With api calls and business logic the response should be 250ms, but with a 250ms renderToString it is doubled! Bad for SEO and users. Also, being a synchronous method, renderToString() can block the node server and backup subsequent requests (this could be solved by using 2 separate node servers: 1 as a web server, and 1 as a service to solely render react).

Attempts

Ideally, it would take 5-50ms to renderToString in production. I've been working on some ideas, but I'm not exactly sure what the best approach would be.

Idea 1: Caching components

Any component that is marked as 'static' could be cached. By keeping a cache with the rendered markup, the renderToString() could check the cache before rendering. If it finds a component, it automatically grabs the string. Doing this at a high level component would save all the nested children component's mounting. You would have to replace the cached component markup's react rootID with the current rootID.

Idea 2: Marking components as simple/dumb

By defining a component as 'simple', react should be able to skip all the lifecycle methods when rendering. React already does this for the core react dom components (<p/>, <h1/>, etc). Would be nice to extend custom components to use the same optimization.

Idea 3: Skip components on server-side render

Components that do not need to be returned by the server (no SEO value) could simply be skipped on the server. Once the client loads, set a clientLoaded flag to true and pass it down to enforce a re-render.

Closing and other attempts

The only solution I've implemented thus far is to reduce the number of components that are rendered on the server.

Some projects we're looking at include:

Has anybody faced similar issues? What have you been able to do? Thanks.

4
This is an interesting question. Have you encountered a performance problem in need of solving? If so, have you been able to localize the problem to specific subtrees?Josh David Miller
For some of my more complex pages, there are 1500+ components that are being rendered (since react breaks down everything into a 'component'). Anywhere with around 1000+ components seems to be pretty slow. I messed around with the react source code with ReactCompositeComponent, and add components to a cache if they match certain criteria. However, this results in an invariant when pulling from the cache, since each component has an incorrect root react id. it did however vastly improve performance if i'm pulling the resultant string from a cache, rather than mounting it ;)Jon
I assumed by "component" you meant ones you had defined, rather than built-in jsx tags. If including those, then 1500 isn't totally unreasonable. But anyway, I'm still trying to get to the root of the problem. Is the slowness on the data layer or on the computation layer? If all data was in memory, would the problem still occur?Josh David Miller
I don't have an answer to the caching problem, but I hope someone here does. In the meantime, I'd try to locate which trees are problematic and attempt to optimize those (e.g. use pure, stateless components where possible to avoid extra lifecycle methods and so forth). For a particularly large page, I would also look at pre-rendering only the critical path and then loading the rest on the client side. This could affect SEO, but probably not on Google. And you could pre-render more for crawlers who self-identify.Josh David Miller
Re. idea 2, React already supports stateless functional components. These only have a render method, no state and definitely no lifecycle methods. I doubt if react does memoization of these components, but that may change in future.hazardous

4 Answers

13
votes

Using react-router1.0 and react0.14, we were mistakenly serializing our flux object multiple times.

RoutingContext will call createElement for every template in your react-router routes. This allows you to inject whatever props you want. We also use flux. We send down a serialized version of a large object. In our case, we were doing flux.serialize() within createElement. The serialization method could take ~20ms. With 4 templates, that would be an extra 80ms to your renderToString() method!

Old code:

function createElement(Component, props) {
    props = _.extend(props, {
        flux: flux,
        path: path,
        serializedFlux: flux.serialize();
    });
    return <Component {...props} />;
}
var start = Date.now();
markup = renderToString(<RoutingContext {...renderProps} createElement={createElement} />);
console.log(Date.now() - start);

Easily optimized to this:

var serializedFlux = flux.serialize(); // serialize one time only!

function createElement(Component, props) {
    props = _.extend(props, {
        flux: flux,
        path: path,
        serializedFlux: serializedFlux
    });
    return <Component {...props} />;
}
var start = Date.now();
markup = renderToString(<RoutingContext {...renderProps} createElement={createElement} />);
console.log(Date.now() - start);

In my case this helped reduce the renderToString() time from ~120ms to ~30ms. (You still need to add the 1x serialize()'s ~20ms to the total, which happens before the renderToString()) It was a nice quick improvement. -- It's important to remember to always do things correctly, even if you don't know the immediate impact!

8
votes

Idea 1: Caching components

Update 1: I've added a complete working example at the bottom. It caches components in memory and updates data-reactid.

This can actually be done easily. You should monkey-patch ReactCompositeComponent and check for a cached version:

import ReactCompositeComponent from 'react/lib/ReactCompositeComponent';
const originalMountComponent = ReactCompositeComponent.Mixin.mountComponent;
ReactCompositeComponent.Mixin.mountComponent = function() {
    if (hasCachedVersion(this)) return cache;
    return originalMountComponent.apply(this, arguments)
}

You should do this before you require('react') anywhere in your app.

Webpack note: If you use something like new webpack.ProvidePlugin({'React': 'react'}) you should change it to new webpack.ProvidePlugin({'React': 'react-override'}) where you do your modifications in react-override.js and export react (i.e. module.exports = require('react'))

A complete example that caches in memory and updates reactid attribute could be this:

import ReactCompositeComponent from 'react/lib/ReactCompositeComponent';
import jsan from 'jsan';
import Logo from './logo.svg';

const cachable = [Logo];
const cache = {};

function splitMarkup(markup) {
    var markupParts = [];
    var reactIdPos = -1;
    var endPos, startPos = 0;
    while ((reactIdPos = markup.indexOf('reactid="', reactIdPos + 1)) != -1) {
        endPos = reactIdPos + 9;
        markupParts.push(markup.substring(startPos, endPos))
        startPos = markup.indexOf('"', endPos);
    }
    markupParts.push(markup.substring(startPos))
    return markupParts;
}

function refreshMarkup(markup, hostContainerInfo) {
    var refreshedMarkup = '';
    var reactid;
    var reactIdSlotCount = markup.length - 1;
    for (var i = 0; i <= reactIdSlotCount; i++) {
        reactid = i != reactIdSlotCount ? hostContainerInfo._idCounter++ : '';
        refreshedMarkup += markup[i] + reactid
    }
    return refreshedMarkup;
}

const originalMountComponent = ReactCompositeComponent.Mixin.mountComponent;
ReactCompositeComponent.Mixin.mountComponent = function (renderedElement, hostParent, hostContainerInfo, transaction, context) {
    return originalMountComponent.apply(this, arguments);
    var el = this._currentElement;
    var elType = el.type;
    var markup;
    if (cachable.indexOf(elType) > -1) {
        var publicProps = el.props;
        var id = elType.name + ':' + jsan.stringify(publicProps);
        markup = cache[id];
        if (markup) {
            return refreshMarkup(markup, hostContainerInfo)
        } else {
            markup = originalMountComponent.apply(this, arguments);
            cache[id] = splitMarkup(markup);
        }
    } else {
        markup = originalMountComponent.apply(this, arguments)
    }
    return markup;
}
module.exports = require('react');
6
votes

It's not a complete solution I had the same issue, with my react isomorphic app, and I used a couple of things.

  1. Use Nginx in front of your nodejs server, and cache the rendered response for a short time.

  2. In Case of showing a list of items, I use only a subset of list. For example, I will render only X items to fill up the viewport, and load the rest of the list in the client side using Websocket or XHR.

  3. Some of my components are empty in serverside rendering and will only load from client side code (componentDidMount). These components are usually graphs or profile related components. Those components usually don't have any benefit from SEO point of view

  4. About SEO, from my experience 6 Month with an isomorphic app. Google Bot can read Client side React Web page easily, so I'm not sure why we bother with the server side rendering.

  5. Keep the <Head>and <Footer> as static string or use template engine (Reactjs-handlebars), and render only the content of the page, (it should save a few rendered components). In case of a single page app, you can update the title description in each navigation inside Router.Run.

4
votes

I think fast-react-render can help you. It increases the performance of your server rendering three times.

For try it, you only need to install package and replace ReactDOM.renderToString to FastReactRender.elementToString:

var ReactRender = require('fast-react-render');

var element = React.createElement(Component, {property: 'value'});
console.log(ReactRender.elementToString(element, {context: {}}));

Also you can use fast-react-server, in that case render will be 14 times as fast as traditional react rendering. But for that each component, which you want to render, must be declared with it (see an example in fast-react-seed, how you can do it for webpack).