React renderToString() Performance and Caching React Components React renderToString() Performance and Caching React Components reactjs reactjs

React renderToString() Performance and Caching React Components


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!


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');


It's not a complete solutionI 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.