• Stars
    star
    821
  • Rank 53,248 (Top 2 %)
  • Language
    JavaScript
  • License
    Apache License 2.0
  • Created almost 8 years ago
  • Updated over 7 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

React.js server-side rendering optimization with component memoization and templatization

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.

More Repositories

1

lacinia

GraphQL implementation in pure Clojure
Clojure
1,798
star
2

thorax

Strengthening your Backbone
JavaScript
1,324
star
3

electrode

Electrode - Application Platform on React/Node.js powering Walmart.com
446
star
4

little-loader

A lightweight, IE8+ JavaScript loader
JavaScript
371
star
5

json-to-simple-graphql-schema

Transforms JSON input into a GraphQL schema
JavaScript
279
star
6

eslint-config-defaults

A composable set of ESLint defaults
JavaScript
229
star
7

lumbar

Modular javascript build tool
JavaScript
226
star
8

datascope

Visualization of Clojure data structures using Graphviz
Clojure
206
star
9

lacinia-pedestal

Expose Lacinia GraphQL as Pedestal endpoints
Clojure
197
star
10

bigben

BigBen - a generic, multi-tenant, time-based event scheduler and cron scheduling framework
Kotlin
196
star
11

concord

Concord - workflow orchestration and continuous deployment management
Java
195
star
12

kubeman

The Hero that Kubernetes deserves
TypeScript
164
star
13

system-viz

Graphviz visualization of a component system
Clojure
159
star
14

react-native-orientation-listener

A react-native library for obtaining current device orientation
Java
151
star
15

thorax-seed

JavaScript
131
star
16

vizdeps

Visualize Leiningen dependencies using Graphviz
Clojure
130
star
17

mupd8

Muppet
Scala
126
star
18

active-status

Present status of mulitple 'jobs' in a command line tool, using terminal capability codes
Clojure
118
star
19

schematic

Combine configuration with building a Component system
Clojure
104
star
20

easy-fix

easy-fix: run integration tests like unit tests
JavaScript
101
star
21

dyn-edn

Dynamic properties in EDN content
Clojure
94
star
22

fruit-loops

Server-side jQuery API renderer.
JavaScript
89
star
23

generator-thorax

Thorax yeoman generator
JavaScript
87
star
24

walmart-cla

Walmart Contributor License Agreement Information
85
star
25

react-native-cropping

Cropping components for react-native
JavaScript
68
star
26

curved-carousel

An infinitely scrolling carousel with configurable curvature
JavaScript
64
star
27

walmart-api

API wrapper for the public Walmart Labs API
JavaScript
62
star
28

gozer

Open source library to parse various X12 file formats for retail/supply chain
Java
60
star
29

cookie-cutter

An opinionated micro-services framework for TypeScript
TypeScript
57
star
30

mock-server

SPA application debug proxy server
JavaScript
56
star
31

test-reporting

Tiny library to assist with reporting some context when a test fails
Clojure
53
star
32

container-query

A responsive layout helper based on the width of the container
JavaScript
52
star
33

react-native-platform-visible

A very simple component visibility switch based on Platform
JavaScript
49
star
34

eslint-config-walmart

A set of default eslint configurations, Walmart Labs style.
JavaScript
48
star
35

clojure-game-geek

Example source code for the Lacinia tutorial
Clojure
47
star
36

babel-plugin-react-cssmoduleify

Babel plugin to transform traditional React element classNames to CSS Modules
JavaScript
45
star
37

generator-release

Yeoman generator for handling Bower/NPM releases.
JavaScript
45
star
38

costanza

Frontend error tracking toolkit: Own your own domain
JavaScript
35
star
39

zFAM

z/OS-based File Access Manager
Assembly
35
star
40

nightcall

Automated Enumeration Script for Pentesting
Python
34
star
41

cond-let

A useful merge of cond and let
Clojure
30
star
42

static

JavaScript
26
star
43

bolt

[DEPRECATED] an opinionated meta task runner for components.
JavaScript
24
star
44

shared-deps

Leiningen plugin to allow sub-modules to more easily share common dependencies
Clojure
24
star
45

ridicule

Mocking everything
JavaScript
22
star
46

linearroad

Walmart version of the Linear Road streaming benchmark.
Java
22
star
47

backbone-historytracker

Backbone plugin for navigation direction tracking
JavaScript
22
star
48

zECS

z/OS-based Enterprise Cache Service
COBOL
21
star
49

pulsar

Text-based dashboard for Elixir CLIs
Elixir
20
star
50

react-native-image-progressbar

An image based progress bar
JavaScript
20
star
51

layout

A simple responsive layout helper
CSS
17
star
52

zUID

z/OS-based Unique Identifier generator
Assembly
16
star
53

showcase-template

A starter template for a showcase of React components
CSS
16
star
54

grunt-release-component

Grunt release helper for bower components
JavaScript
15
star
55

anomaly-detection-walmart

Python
14
star
56

partnerapi_sdk_dotnet

Walmart Partner API SDK for .NET
C#
14
star
57

circus

External Webpack Component Plugin
JavaScript
13
star
58

concord-website

Documentation website source code for Concord
JavaScript
13
star
59

concord-plugins

Java
12
star
60

strati-functional

A lightweight collection of functional classes used to complement core Java.
Java
12
star
61

thorax-boilerplate

A boilerplate project for Thoroax
JavaScript
11
star
62

nocktor

nocktor - your nock doctor
JavaScript
11
star
63

getting-started

How to get started with the WalmartLabs API and tooling
9
star
64

LinearGenerator

Reworked data generator for LinearRoad streaming benchmark that no longer needs mitsim or any database.
Java
9
star
65

json-patchwork

JavaScript
9
star
66

object-diff

A Go library implementing object wise diff and patch.
Go
8
star
67

small-world-graph

Graphing as in Data
C++
8
star
68

chai-shallowly

A chai assertion plugin for enzyme.
JavaScript
8
star
69

typeahead.js-legacy

typeahead.js is a fast and fully-featured autocomplete library
JavaScript
8
star
70

child-pool

child_process pool implementation
JavaScript
7
star
71

thorax-todos

JavaScript
6
star
72

hula-hoop

Server-side rendering components for Thorax + Hapi stacks.
JavaScript
6
star
73

apache-http-client

A Clojure ring compatible http interface for Apache HttpClient
Clojure
5
star
74

lumbar-loader

JavaScript
5
star
75

krabby

JavaScript
4
star
76

js-conceptualizer

JS Conceptualizer is a bit of client-side Javascript that parses out concepts, particularly proper nouns, from HTML on a web page.
JavaScript
4
star
77

babel-plugin-i18n-id-hashing

Namespace the ID of React-Intl keys
JavaScript
4
star
78

express-example

A crazy simple example of using the Walmart API with express
JavaScript
4
star
79

wml-coding-std

A module for keeping consistent JS coding standards at WML
JavaScript
3
star
80

nativedriver

native driver for UI automation
Objective-C
3
star
81

was

Go
3
star
82

thorax-rails

Ruby
3
star
83

hapi-example

A crazy simple example of using the Walmart API with hapi
JavaScript
3
star
84

lumbar-long-expires

Long expires cache buster plugin for Lumbar
JavaScript
3
star
85

component-scan

A component scanner for React
JavaScript
3
star
86

scanomatic-server

Scan-O-Matic Node Server
JavaScript
3
star
87

hadoop-openstack-swifta

hadoop-openstack-swifta
Java
3
star
88

SDJSBridge

Native/Hybrid Javascript Bridge
Objective-C
2
star
89

cordova-starter-kit

A starter kit for using Cordova and the Walmart API
JavaScript
2
star
90

priorityY

Priority Based Connected Components
Python
2
star
91

bolt-standard-flux

electrode bolt standard configs and tasks for flux architecture.
JavaScript
1
star
92

github-util

Github utility methods.
JavaScript
1
star
93

lumbar-tester

Unit testing plugin for Lumbar
JavaScript
1
star
94

apidocs

HTML
1
star
95

circus-stylus

Stylus linker for Circus components
JavaScript
1
star
96

SDUserActivity

An simplified interface for NSUserActivity for apps that want to participate in handoff.
Objective-C
1
star
97

walmartlabs.github.io

Helper to redirect to code.walmartlabs.com
HTML
1
star
98

BurpSuiteDynamicSessionTracker

BurpSuite extension for tracking and manipulating dynamic session cookies
Java
1
star
99

bolt-cli

[DEPRECATED] bolt command line interface.
JavaScript
1
star
100

phoenix-build

Common build libraries for Phoenix projects
JavaScript
1
star