Code Monkey home page Code Monkey logo

react-ssr-optimization's Introduction

React Server-Side Rendering Optimization Library

This React Server-side optimization library is a configurable ReactJS extension for memoizing react component markup on the server. It also supports component templatization to further caching of rendered markup with more dynamic data. This server-side module intercepts React's instantiateReactComponent module by using a require() hook and avoids forking React.

Build Status version License

Why we built it

React is a best-of-breed UI component framework allowing us to build higher level components that can be shared and reused across pages and apps. React's Virtual DOM offers an excellent development experience, freeing us up from having to manage subtle DOM changes. Most importantly, React offers us a great out-of-the-box isomorphic/universal JavaScript solution. React's renderToString(..) can fully render the HTML markup of a page to a string on the server. This is especially important for initial page load performance (particularly for mobile users with low bandwidth) and search engine indexing and ranking — both for SEO (search engine optimization) and SEM (search engine marketing).

However, it turns out that React’s server-side rendering can become a performance bottleneck for pages requiring many virtual DOM nodes. On large pages, ReactDOMServer.renderToString(..) can monopolize the CPU, block node’s event-loop and starve out incoming requests to the server. That’s because for every page request, the entire page needs to be rendered, even fine-grained components — which given the same props, always return the same markup. CPU time is wasted in unnecessarily re-rendering the same components for every page request. Similar to pure functions in functional programing a pure component will always return the same HTML markup given the same props. Which means it should be possible to memoize (or cache) the rendered results to speed up rendering significantly after the first response.

We also wanted the ability to memoize any pure component, not just those that implement a certain interface. So we created a configurable component caching library that accepts a map of component name to a cacheKey generator function. Application owners can opt into this optimization by specifying the component's name and referencing a cacheKey generator function. The cacheKey generator function returns a string representing all inputs into the component's rendering that is then used to cache the rendered markup. Subsequent renderings of the component with the same name and the same props will hit the cache and return the cached result. This optimization lowers CPU time for each page request and allows more concurrent requests that are not blocked on synchronous renderToString calls. The CPU profiles we took after before and after applying these optimizations show significant reduction no CPU utilization for each request.

YouTube: Hastening React SSR with Component Memoization and Templatization

To learn more about why we built this library, check out a talk from the Full Stack meetup from July 2016:

YouTube: Hastening React SSR with Component Memoization and Templatization

As well as another (lower quality) recording from the San Diego Web Performance meetup from August 2016:

YouTube: Hastening React SSR with Component Memoization and Templatization

Slide Deck: Hastening React SSR with component memoization and templatization

How we built it

After peeling through the React codebase we discovered React’s mountComponent function. This is where the HTML markup is generated for a component. We knew that if we could intercept React's instantiateReactComponent module by using a require() hook we could avoid the need to fork React and inject our optimization. We keep a Least-Recently-Used (LRU) cache that stores the markup of rendered components (replacing the data-reactid appropriately).

We also implemented an enhancement that will templatize the cached rendered markup to allow for more dynamic props. Dynamic props are replaced with template delimiters (i.e. ${ prop_name }) during the react component rendering cycle. The template is them compiled, cached, executed and the markup is handed back to React. For subsequent requests the component's render(..) call is short-circuited with an execution of the cached compiled template.

How you install it

npm install --save react-ssr-optimization

How you use it

You should load the module in the first script that's executed by Node, typically index.js.

In index.js you will have code that looks something like this:

"use strict";

var componentOptimization = require("react-ssr-optimization");

var keyGenerator = function (props) {
    return props.id + ":" + props.name;
};

var componentOptimizationRef = componentOptimization({
    components: {
      'Component1': keyGenerator,
      'Component2': {
        cacheKeyGen: keyGenerator,
      },
    },
    lruCacheSettings: {
        max: 500,  //The maximum size of the cache
    }
});

With the cache reference you can also execute helpful operational functions like these:

//can be turned off and on dynamically by calling the enable function.
componentOptimizationRef.enable(false);
// Return an array of the cache entries
componentOptimizationRef.cacheDump();
// Return total length of objects in cache taking into account length options function.
componentOptimizationRef.cacheLength();
// Clear the cache entirely, throwing away all values.
componentOptimizationRef.cacheReset();

How you use component templatization

Even though pure components ‘should’ always render the same markup structure there are certain props that might be more dynamic than others. Take for example the following simplified product react component.

var React = require('react');

var ProductView = React.createClass({
  render: function() {
    return (
      <div className="product">
        <img src={this.props.product.image}/>
        <div className="product-detail">
          <p className="name">{this.props.product.name}</p>
          <p className="description">{this.props.product.description}</p>
          <p className="price">Price: ${this.props.selected.price}</p>
          <button type="button" onClick={this.addToCart} disabled={this.props.inventory > 0 ? '' : 'disabled'}>
            {this.props.inventory ? 'Add To Cart' : 'Sold Out'}
          </button>        
        </div>
      </div>
    );
  }
});

module.exports = ProductView;

This component takes props like product image, name, description, price. If we were to apply the component memoization described above, we’d need a cache large enough to hold all the products. Moreover, less frequently accessed products would likely to have more cache misses. This is why we also added the component templatization feature. This feature requires classifying properties in two different groups:

  • Template Attributes: Set of properties that can be templatized. For example in a component, the url and label are template attributes since the structure of the markup does not change with different url and label values.
  • Cache Key Attributes: Set of properties that impact the rendered markup. For example, availabilityStatus of a item impacts the resulting markup from generating a ‘Add To Cart’ button to ‘Get In-stock Alert’ button along with pricing display etc.

These attributes are configured in the component caching library, but instead of providing a cacheKey generator function you’d pass in the templateAttrs and cacheAttrs instead. It looks something like this:

var componentOptimization = require("react-ssr-optimization");

componentOptimization({
    components: {
      "ProductView": {
        templateAttrs: ["product.image", "product.name", "product.description", "product.price"],
        cacheAttrs: ["product.inventory"]
      },
      "ProductCallToAction": {
        templateAttrs: ["url"],
        cacheAttrs: ["availabilityStatus", "isAValidOffer", "maxQuantity", "preorder", "preorderInfo.streetDateType", "puresoi", "variantTypes", "variantUnselectedExp"]
      }
    }
});

Notice that the template attributes for ProductView are all the dynamic props that would be different for each product. In this example, we also used product.inventory prop as a cache key attribute since the markup changes based on inventory logic to enable the add to cart button. Here is the same product component from above cached as a template.

<div className="product">
  <img src=${product_image}/>
  <div className="product-detail">
    <p className="name">${product_name}</p>
    <p className="description">${product_description}</p>
    <p className="price">Price: ${selected_price}</p>
    <button type="button" onClick={this.addToCart} disabled={this.props.inventory > 0 ? '' : 'disabled'}>
      {this.props.inventory ? 'Add To Cart' : 'Sold Out'}
    </button>        
  </div>
</div>

For the given component name, the cache key attributes are used to generate a cache key for the template. For subsequent requests the component’s render is short-circuited with a call to the compiled template.

How you configure it

Here are a set of option that can be passed to the react-ssr-optimization library:

  • components: A required map of components that will be cached and the corresponding function to generate its cache key.
    • key: a required string name identifying the component. This can be either the name of the component when it extends React.Component or the displayName variable.
    • value: a required function/object which generates a string that will be used as the component's CacheKey. If an object, it can contain the following attributes
      • cacheKeyGen: an optional function which generates a string that will be used as the component's CacheKey. If cacheKeyGen and cacheAttrs are not set, then only one element for the component will exist in the cache
      • templateAttrs: an optional array of strings corresponding to attribute name/key in props that need to be templatized. Each value can have deep paths ex: x.y.z
      • cacheAttrs: an optional array of attributes to be used for generating a cache key. Can be used in place of cacheKeyGen.
  • lruCacheSettings: By default, this library uses a Least Recently Used (LRU) cache to store rendered markup of cached components. As the name suggests, LRU caches will throw out the data that was least recently used. As more components are put into the cache other rendered components will fall out of the cache. Configuring the LRU cache properly is essential for server optimization. Here are the LRU cache configurations you should consider setting:
    • max: an optional number indicating the maximum size of the cache, checked by applying the length function to all values in the cache. Default value is Infinity.
    • maxAge: an optional number indicating the maximum age in milliseconds. Default value is Infinity.
    • length: an optional function that is used to calculate the length of stored items. The default is function(){return 1}.
  • cacheImpl: an optional config that allows the usage of a custom cache implementation. This will take precedence over the lruCacheSettings option.
  • disabled: an optional config indicating that the component caching feature should be disabled after instantiation.
  • eventCallback: an optional function that is executed for interesting events like cache miss and hits. The function should take an event object function(e){...}. The event object will have the following properties:
    • type: the type of event, e.g. "cache".
    • event: the kind of event, e.g. "miss" for cache events.
    • cmpName: the component name that this event transpired on, e.g. "Hello World" component.
    • loadTimeNS: the load time spent loading/generating a value for a cache miss, in nanoseconds. This only returns a value when collectLoadTimeStats option is enabled.
  • collectLoadTimeStats: an optional config indicating enabling the loadTimeNS stat to be calculated and returned in the eventCallback cache miss events.

Other Performance Approaches

It is important to note that there are several other independent projects that are endeavoring to solve the React server-side rendering bottleneck. Projects like react-dom-stream and react-server attempt to deal with the synchronous nature of ReactDOM.renderToString by rendering React pages asynchronously and in separate chunks. Streaming and chunking react rendering helps on the server by preventing synchronous render processing from starving out other concurrent requests. Streaming the initial HTML markup also means that browsers can start painting pages earlier (without having to wait for the entire response).

These approaches help improve user perceived performance since content can be painted sooner on the screen. But whether rendering is done synchronously or asynchronously, the total CPU time remains the same since the same amount of work still needs to be done. In contrast, component memoization and templatization reduces the total amount of CPU time for subsequent requests that re-render the same components again. These rendering optimizations can be used in conjunction with other performance enhancements like asynchronous rendering.

react-ssr-optimization's People

Contributors

ananavati avatar maximenajim avatar mnaga avatar thabti avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

react-ssr-optimization's Issues

Cached template strings causes issues with escaped strings

When using the cache on a particular component the cache causes the server to return some unexpected behaviour.

example: const MyComponent = (props) => (<div>{props.name}</div>)

MyComponent: {
  templateAttrs: ['name']
}

Now on the server side, I pass in something with a html equivalent value:
<MyComponent name={'My Name & name 2'} />

this gets put in the cache as something like: <div>${name}</div>

When MyComponent is pulled from the cache and string replaced, &amp; is placed instead of &.
The next time the component is rendered it returns &amp;amp; If caching is not enabled the problem is not present.

TypeError: Can't add property _instantiateReactComponent, object is not extensible

Firstly, I think this framework is a great idea. Totally solved our concern of react's sever rendering. Great work!

My problem is : I run a react server with this framework on my local machine. I don't want change the NODE_ENV value. But I still need to test if this is functioning. So I change the original code of this line
https://github.com/walmartlabs/react-ssr-optimization/blob/master/lib/index.js#L47
to if (false && process.env.NODE_ENV !== "production") {:

But I got this:
TypeError: Can't add property _instantiateReactComponent, object is not extensible

How come? Is it related to my react's version? My react's version is 15,

Module is not working with react and react-dom 16.4.0

There is an error for missing reference of following 2 references.

const InstantiateReactComponent = require("react-dom/lib/instantiateReactComponent");
const escapeTextContentForBrowser = require("react-dom/lib/escapeTextContentForBrowser");

Is it possible for you guys to update this package for react and react-dom 16.4.0?
I am also happy to raise a PR.

Alternative?

The idea is sound. Is there any up to date alternative to this lib on the market? One that supports React 18

React streaming

Does this project work with React streaming, i.e renderToNodeStream ?

Webpack integration

I'm having issues using this module with webpack. For example, here is my app:

const componentOptimization = require('react-ssr-optimization');
import React from 'react';
import ReactDomServer from 'react-dom/server';

const componentOptimizationRef = componentOptimization({
  components: {
    HelloWorld: function (props) {
      console.log('in HelloWorld cache check function'); // eslint-disable-line
      return props.text;
    }
  },
  lruCacheSettings: {
    max: 500
  }
});

let renderCount = 0;

class HelloWorld extends React.Component {
  render() {
    renderCount++;
    return React.DOM.div(null, this.props.text);
  }
}

HelloWorld.propTypes = {
  text: React.PropTypes.string
};

// Cache Miss
ReactDomServer.renderToString(React.createFactory(HelloWorld)({text: "Hello World X!"}));
console.log('renderCount is', renderCount);

// Cache Hit
ReactDomServer.renderToString(React.createFactory(HelloWorld)({text: "Hello World X!"}));
console.log('renderCount is', renderCount);

setInterval(() => {
  console.log('cache length is', componentOptimizationRef.cacheLength()); // eslint-disable-line
}, 3000);

After bundling and executing, this is the output:

renderCount is 1
renderCount is 2
cache length is 0
cache length is 0
cache length is 0
cache length is 0
cache length is 0
...

Avoid caching a component because of a prop

So, imagine I've a component that changes his behaviour for logged users. I think that's not worth it to cache that component as it will literally fill the cache with components that are not used as some general ones. Is there any way to avoid caching by a prop? For example, returning false?

If not, is it something that you consider? Is something that if you get a PR will be glad to merge?

Thanks!

Wrong images URL in the HTML rendered by the server

The server does not render the right URLs for images. e.g. we get:

<img src="/5d5d9eefa31e5e13a6610d9fa7a283bb.svg" ... />

instead of

<img src="/static/media/logo.5d5d9eef.svg" ... />

So the page looks broken while the JS is loaded....

Cache All Option?

If you have plenty of components it's hard to keep track of everything, and It seems to me that you can actually cache everything with a template type of cache and it should work or I'm missing something?

Thanks

Feature Request: Allow environment setting REACT_SSR_OPTIMIZATION_ENV with fallback to NODE_ENV

Before we start working with this library I wanted to test out the result of caching locally to make sure I am setting the caching strategy variables templateAttrs and cacheAttrs correctly i.e. I want to make sure I don't miss a variable in cacheAttrs that could cause a incorrect cached response. This is also something that would be helpful to other developers when making component changes so they can confirm they did not break the caching strategy or to write tests around the caching strategy. Currently the library uses NODE_ENV exclusively, but it would be helpful to have a specific env var maybe REACT_SSR_OPTIMIZATION_ENV with a fallback to NODE_ENV. Looks like all that would have to change is this line https://github.com/walmartlabs/react-ssr-optimization/blob/master/lib/index.js#L44 so if this makes sense let me know and I can submit a PR.

Is react and react-dom required in package.json?

Have react and react-dom listed in package.json causes NPM to create a local copy of the library. Running two versions of React then create an incompatibility between the library and React.
This was tested with the main application package.json containing version ^15.3.0 and electrode-react-srr-optimiziation containing ^0.14.8.

I understand versioning is required so you can maintain compatibility with released React versions but in this instance it has caused an incompatibility.

Compatibility with Webpack and Babel

Brilliant work. I'm currently trying to get this working on a basic level in a project with Webpack and Babel.
It's an universal app and I have babel initialised as require('babel-register') ({}). Then shortly after I have the implementation of electrode-react-ssr-optimization. I then use imports instead of require.
The issue was the cache is returning 0 length and the key generator function appears to have not been called.
I believe webpack might be using it's own require code on transpilation. Was wondering if you had any experience with this before I go digging in.

Cache based on context

Looking at the keyGenerator function in the docs, it appears to have this method signature:

function keyGenerator(props) { ... }

Is it possible to inspect context as well? I would need to include data from the context in my key.

Thanks!

Not compatible with React 15.*

Hi!

We want to try your package in production but unfortunately it only works with React v14. Will it be compatible with new versions?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.