├── .babelrc
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── docs
├── art-of-optimizing-ssr.md
└── sample-template.md
├── gulpfile.js
├── lib
├── cache-store.js
├── config.js
└── ssr-caching.js
├── package.json
└── test
├── .eslintrc
├── farmhash-mock.js
├── spec
├── cache-store.spec.js
├── caching.simple.spec.js
├── caching.template.spec.js
├── hash-key.spec.js
└── profiling.spec.js
└── src
├── board.jsx
├── greeting.jsx
├── heading.jsx
├── hello.jsx
├── info-card.jsx
├── recursive-divs.jsx
├── render-board.jsx
├── render-greeting.jsx
└── render-hello.jsx
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "es2015",
4 | "es2015-loose",
5 | "react"
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | test/gen-lib
2 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | ---
2 | extends:
3 | - "./node_modules/electrode-archetype-njs-module-dev/config/eslint/.eslintrc-node"
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.gitignore.io/api/gitbook,osx,webstorm,node
3 |
4 | ### GitBook ###
5 | # Node rules:
6 | ## Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
7 | .grunt
8 |
9 | ## Dependency directory
10 | ## Commenting this out is preferred by some people, see
11 | ## https://docs.npmjs.com/misc/faq#should-i-check-my-node_modules-folder-into-git
12 | node_modules
13 |
14 | # Book build output
15 | _book
16 |
17 | # eBook build output
18 | *.epub
19 | *.mobi
20 | *.pdf
21 |
22 |
23 | ### OSX ###
24 | .DS_Store
25 | .AppleDouble
26 | .LSOverride
27 |
28 | # Icon must end with two \r
29 | Icon
30 |
31 |
32 | # Thumbnails
33 | ._*
34 |
35 | # Files that might appear in the root of a volume
36 | .DocumentRevisions-V100
37 | .fseventsd
38 | .Spotlight-V100
39 | .TemporaryItems
40 | .Trashes
41 | .VolumeIcon.icns
42 |
43 | # Directories potentially created on remote AFP share
44 | .AppleDB
45 | .AppleDesktop
46 | Network Trash Folder
47 | Temporary Items
48 | .apdisk
49 |
50 |
51 | ### WebStorm ###
52 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
53 |
54 | *.iml
55 |
56 | ## Directory-based project format:
57 | .idea/
58 | # if you remove the above rule, at least ignore the following:
59 |
60 | # User-specific stuff:
61 | # .idea/workspace.xml
62 | # .idea/tasks.xml
63 | # .idea/dictionaries
64 | # .idea/shelf
65 |
66 | # Sensitive or high-churn files:
67 | # .idea/dataSources.ids
68 | # .idea/dataSources.xml
69 | # .idea/sqlDataSources.xml
70 | # .idea/dynamic.xml
71 | # .idea/uiDesigner.xml
72 |
73 | # Gradle:
74 | # .idea/gradle.xml
75 | # .idea/libraries
76 |
77 | # Mongo Explorer plugin:
78 | # .idea/mongoSettings.xml
79 |
80 | ## File-based project format:
81 | *.ipr
82 | *.iws
83 |
84 | ## Plugin-specific files:
85 |
86 | # IntelliJ
87 | /out/
88 |
89 | # mpeltonen/sbt-idea plugin
90 | .idea_modules/
91 |
92 | # JIRA plugin
93 | atlassian-ide-plugin.xml
94 |
95 | # Crashlytics plugin (for Android Studio and IntelliJ)
96 | com_crashlytics_export_strings.xml
97 | crashlytics.properties
98 | crashlytics-build.properties
99 | fabric.properties
100 |
101 |
102 | ### Node ###
103 | # Logs
104 | logs
105 | *.log
106 | npm-debug.log*
107 |
108 | # Runtime data
109 | pids
110 | *.pid
111 | *.seed
112 |
113 | # Directory for instrumented libs generated by jscoverage/JSCover
114 | lib-cov
115 |
116 | # Coverage directory used by tools like istanbul
117 | coverage
118 |
119 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
120 | .grunt
121 |
122 | # node-waf configuration
123 | .lock-wscript
124 |
125 | # Compiled binary addons (http://nodejs.org/api/addons.html)
126 | build/Release
127 |
128 | # Dependency directory
129 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
130 | node_modules
131 |
132 | # Optional npm cache directory
133 | .npm
134 |
135 | # Optional REPL history
136 | .node_repl_history
137 |
138 | .tmp
139 |
140 | test/gen-lib
141 |
142 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | node_js:
4 | - v6
5 |
6 | brances:
7 | only:
8 | - master
9 |
10 | before_install:
11 | - currentfolder=${PWD##*/}
12 | - if [ "$currentfolder" != 'electrode-react-ssr-caching' ]; then cd .. && eval "mv $currentfolder electrode-react-ssr-caching" && cd electrode-react-ssr-caching; fi
13 |
14 | script:
15 | - npm test
16 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2016 WalmartLabs
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # electrode-react-ssr-caching [![NPM version][npm-image]][npm-url] [![Build Status][travis-image]][travis-url] [![Dependency Status][daviddm-image]][daviddm-url]
2 |
3 | Support profiling React Server Side Rendering time and component caching to help you speed up SSR.
4 |
5 | # Installing
6 |
7 | ```
8 | npm i electrode-react-ssr-caching
9 | ```
10 |
11 | # Usage
12 |
13 | Note that since this module patches React's source code to inject the caching logic, it must be loaded before the React module.
14 |
15 | For example:
16 |
17 | ```js
18 | import SSRCaching from "electrode-react-ssr-caching";
19 | import React from 'react';
20 | import ReactDOM from 'react-dom/server';
21 | ```
22 |
23 |
24 | ## Profiling
25 |
26 | You can use this module to inspect the time each component took to render.
27 |
28 | ```js
29 | import SSRCaching from "electrode-react-ssr-caching";
30 | import { renderToString } from "react-dom/server";
31 | import MyComponent from "mycomponent";
32 |
33 | // First you should render your component in a loop to prime the JS engine (i.e: V8 for NodeJS)
34 | for( let i = 0; i < 10; i ++ ) {
35 | renderToString();
36 | }
37 |
38 | SSRCaching.clearProfileData();
39 | SSRCaching.enableProfiling();
40 | const html = renderToString();
41 | SSRCaching.enableProfiling(false);
42 | console.log(JSON.stringify(SSRCaching.profileData, null, 2));
43 | ```
44 |
45 | ## Caching
46 |
47 | Once you determined the most expensive components with profiling, you can enable component caching this module provides to speed up SSR performance.
48 |
49 | The basic steps to enabling caching are:
50 |
51 | ```js
52 | import SSRCaching from "electrode-react-ssr-caching";
53 |
54 | SSRCaching.enableCaching();
55 | SSRCaching.setCachingConfig(cacheConfig);
56 | ```
57 |
58 | Where `cacheConfig` contains information on what component to apply caching. See below for details.
59 |
60 | In order for the `enableCaching()` method to work, you'll also need `NODE_ENV` set to `production`, or else it will throw an error.
61 |
62 | ### cacheConfig
63 |
64 | SSR component caching was first demonstrated in [Sasha Aickin's talk].
65 |
66 | His demo requires each component to provide a function for generating the cache key.
67 |
68 | Here we implemented two cache key generation strategies: `simple` and `template`.
69 |
70 | You are required to pass in the `cacheConfig` to tell this module what component to apply caching.
71 |
72 | For example:
73 |
74 | ```js
75 | const cacheConfig = {
76 | components: {
77 | "Component1": {
78 | strategy: "simple",
79 | enable: true
80 | },
81 | "Component2": {
82 | strategy: "template",
83 | enable: true
84 | }
85 | }
86 | }
87 |
88 | SSRCaching.setCachingConfig(cacheConfig);
89 | ```
90 |
91 | ### Caching Strategies
92 |
93 | #### simple
94 |
95 | The `simple` caching strategy is basically doing a `JSON.stringify` on the component's props. You can also specify a callback in `cacheConfig` to return the key.
96 |
97 | For example:
98 |
99 | ```js
100 | const cacheConfig = {
101 | components: {
102 | Component1: {
103 | strategy: "simple",
104 | enable: true,
105 | genCacheKey: (props) => JSON.stringify(props)
106 | }
107 | }
108 | };
109 | ```
110 |
111 | This strategy is not very flexible. You need a cache entry for each different props. However it requires very little processing time.
112 |
113 | #### template
114 |
115 | The `template` caching strategy is more complex but flexible.
116 |
117 | The idea is akin to generating logic-less handlebars template from your React components and then use string replace to process the template with different props.
118 |
119 | If you have this component:
120 |
121 | ```js
122 | class Hello extends Component {
123 | render() {
124 | return
Hello, {this.props.name}. {this.props.message}
125 | }
126 | }
127 | ```
128 |
129 | And you render it with props:
130 | ```js
131 | const props = { name: "Bob", message: "How're you?" }
132 | ```
133 |
134 | You get back HTML string:
135 | ```html
136 | Hello, Bob. How're you?
137 | ```
138 |
139 | Now if you replace values in props with tokens, and you remember that `@0@` refers to `props.name` and `@1@` refers to `props.message`:
140 | ```js
141 | const tokenProps = { name: "@0@", message: "@1@" }
142 | ```
143 |
144 | You get back HTML string that could be akin to a handlebars template:
145 | ```html
146 | Hello, @0@. @1@
147 | ```
148 |
149 | We cache this template html using the tokenized props as cache key. When we need to render the same component with a different props later, we can just lookup the template from cache and use string replace to apply the values:
150 | ```js
151 | cachedTemplateHtml.replace( /@0@/g, props.name ).replace( /@1@/g, props.message );
152 | ```
153 |
154 | That's the gist of the template strategy. Of course there are many small details such as handling the encoding of special characters, preserving props that can't be tokenized, avoid tokenizing non-string props, or preserving `data-reactid` and `data-react-checksum`.
155 |
156 | To specify a component to be cached with the `template` strategy:
157 |
158 | ```js
159 | const cacheConfig = {
160 | components: {
161 | Hello: {
162 | strategy: "template",
163 | enable: true,
164 | preserveKeys: [ "key1", "key2" ],
165 | preserveEmptyKeys: [ "key3", "key4" ],
166 | ignoreKeys: [ "key5", "key6" ],
167 | whiteListNonStringKeys: [ "key7", "key8" ]
168 | }
169 | }
170 | };
171 | ```
172 |
173 | - `preserveKeys` - List of keys that should not be tokenized.
174 | - `preserveEmptyKeys` - List of keys that should not be tokenized if they are empty string `""`
175 | - `ignoreKeys` - List of keys that should be completely ignored as part of the template cache key.
176 | - `whiteListNonStringKeys` - List of non-string keys that should be tokenized.
177 |
178 | # API
179 |
180 | ### [`enableProfiling(flag)`](#enableprofilingflag)
181 |
182 | Enable profiling according to flag
183 |
184 | - `undefined` or `true` - enable profiling
185 | - `false` - disable profiling
186 |
187 | ### [`enableCaching(flag)`](#enablecachingflag)
188 |
189 | Enable cache according to flag
190 |
191 | - `undefined` or `true` - enable caching
192 | - `false` - disable caching
193 |
194 | ### [`enableCachingDebug(flag)`](#enablecachingdebugflag)
195 |
196 | Enable cache debugging according to flag.
197 |
198 | > Caching must be enabled for this to have any effect.
199 |
200 | - `undefined` or `true` - enable cache debugging
201 | - `false` - disable cache debugging
202 |
203 | ### [`setCachingConfig(config)`](#setcachingconfigconfig)
204 |
205 | Set caching config to `config`.
206 |
207 | ### [`stripUrlProtocol(flag)`](#stripurlprotocolflag)
208 |
209 | Remove `http:` or `https:` from prop values that are URLs according to flag.
210 |
211 | > Caching must be enabled for this to have any effect.
212 |
213 | - `undefined` or `true` - strip URL protocol
214 | - `false` - don't strip
215 |
216 | ### [`shouldHashKeys(flag, [hashFn])`](#shouldhashkeysflaghashfn)
217 |
218 | Set whether the `template` strategy should hash the cache key and use that instead.
219 |
220 | > Caching must be enabled for this to have any effect.
221 |
222 | - `flag`
223 | - `undefined` or `true` - use a hash value of the cache key
224 | - `false` - don't use a hash valueo f the cache key
225 | - `hashFn` - optional, a custom callback to generate the hash from the cache key, which is passed in as a string
226 | - i.e. `function customHashFn(key) { return hash(key); }`
227 |
228 | If no `hashFn` is provided, then [farmhash] is used if it's available, otherwise hashing is turned off.
229 |
230 | ### [`clearProfileData()`](#clearprofiledata)
231 |
232 | Clear profiling data
233 |
234 | ### [`clearCache()`](#clearcache)
235 |
236 | Clear caching data
237 |
238 | ### [`cacheEntries()`](#cacheentries)
239 |
240 | Get total number of cache entries
241 |
242 | ### [`cacheHitReport()`](#cachehitreport)
243 |
244 | Returns an object with information about cache entry hits
245 |
246 | Built with :heart: by [Team Electrode](https://github.com/orgs/electrode-io/people) @WalmartLabs.
247 |
248 | [Sasha Aickin's talk]: https://www.youtube.com/watch?v=PnpfGy7q96U
249 | [farmhash]: https://github.com/google/farmhash
250 | [npm-image]: https://badge.fury.io/js/electrode-react-ssr-caching.svg
251 | [npm-url]: https://npmjs.org/package/electrode-react-ssr-caching
252 | [travis-image]: https://travis-ci.org/electrode-io/electrode-react-ssr-caching.svg?branch=master
253 | [travis-url]: https://travis-ci.org/electrode-io/electrode-react-ssr-caching
254 | [daviddm-image]: https://david-dm.org/electrode-io/electrode-react-ssr-caching.svg?theme=shields.io
255 | [daviddm-url]: https://david-dm.org/electrode-io/electrode-react-ssr-caching
256 |
--------------------------------------------------------------------------------
/docs/art-of-optimizing-ssr.md:
--------------------------------------------------------------------------------
1 | # The Art of Optimizing React SSR Performance
2 |
3 | React Server Side Rendering (SSR) enable seamless isomorphic webapps, but SSR is synchronous and CPU bound, so optimizing SSR for isomorphic React app is essential to improving your server's response time.
4 |
5 | There are two chief things you can do to improve your SSR performance: refactoring your component code or component caching.
6 |
7 | If you know your component code well, then you can pick the low hanging fruits and optimize the obvious components. After that, it may become harder to find components to improve, either for refactoring or caching.
8 |
9 | Either way, the next step is to do some detail profiling to find which one to optimize first.
10 |
11 | ## Setup
12 |
13 | To be able to quickly analyze and test your components, it's best to setup some static data that you can use to run the SSR instantly.
14 |
15 | To prepare the data, it's best if you can get real data or craft some sample data that exercise the most parts of you component.
16 |
17 | You can pass your sample data into your component directly, or setup your data model depending on which one you use.
18 |
19 | For example, to pass your data into the component directly as props:
20 |
21 | ```js
22 | const props = {
23 | // comprehensive sample data
24 | };
25 | renderToString();
26 | ```
27 |
28 | If you have async data for your props and you use a resolver or if you use Redux to setup the data, you can manually initialize the data before you render.
29 |
30 | For example, if you use Redux, here is a rough outline, but depending on your specific setup, you may have to create store with middleware etc.
31 |
32 | ```js
33 | // imports for renderToString, createStore, Provider, MyComponent, etc
34 |
35 | function initializeStore(data) {
36 | const store = createStore();
37 | return Promise.resolve((dispatch) => dispatch(data)).then( () => store );
38 | }
39 |
40 | const data = {
41 | // comprehensive sample data to initialize redux store
42 | };
43 |
44 | initializeStore(data)
45 | .then( (store) => renderToString());
46 | ```
47 |
48 | ## Profiling
49 |
50 | After you've written profiling code to manually render your component with static data, you can run the rendering quickly as many times as you like.
51 |
52 | The first thing you want to do now is run profiling for a single rendering pass to find how long each individual component take.
53 |
54 | Here is an example to use electrode-react-ssr-caching to profile a component using Redux:
55 |
56 | ```js
57 | const data = {
58 | // sample data to initialize redux store
59 | };
60 |
61 | // First prime the JS engine
62 | const renderComponent = () => {
63 | return initializeStore(data)
64 | .then( (store) => renderToString());
65 | }
66 |
67 | let promise = renderComponent();
68 |
69 | for( let i = 0; i < 10; i ++ ) {
70 | promise = promise.then( renderComponent );
71 | }
72 |
73 | // Now profile and save the data for a single rendering pass
74 |
75 | promise.then( () => {
76 | SSRCaching.enableProfiling();
77 | SSRCaching.clearProfileData();
78 | return renderComponent()
79 | .then( () => {
80 | console.log( SSRCaching.data, null, 2 );
81 | });
82 | });
83 | ```
84 |
85 | > You should save the profile data to a file rather than log it to the console.
86 |
87 | ## Identifying
88 |
89 | Once you get the JSON data with timing on your components, you can identify what's happening and which components are the most expensive to render.
90 |
91 | Here is a simple example of how the profiling data might look like in YAML format.
92 |
93 | > If you are using Redux you would see Redux's helper components such as `Connect` or `Provider`
94 |
95 | ```yaml
96 | ---
97 | Board:
98 | -
99 | time: 3.652683
100 | Heading:
101 | -
102 | time: 0.379035
103 | InfoCard:
104 | -
105 | time: 1.492614
106 | Hello:
107 | -
108 | time: 0.230108
109 | -
110 | time: 0.30988
111 | Hello:
112 | -
113 | time: 0.095122
114 | -
115 | time: 0.50647
116 | Hello:
117 | -
118 | time: 0.162786
119 | ```
120 |
121 | The time is in milliseconds. As you can see from the above data, here is what it means:
122 |
123 | - Component `Board` took `3.652683ms` to render and it contain children `Heading` and `InfoCard`
124 | - `Heading` took `0.379035ms` to render
125 | - There are 3 instances of `InfoCard`, each one took slightly different time to render and has `Hello` as child.
126 |
127 | > I didn't prime the JS engine when I collected the above data so you can see the first `InfoCard` instance took the longest to render.
128 |
129 | ## Testing
130 |
131 | ## Inspecting
132 |
133 |
134 |
--------------------------------------------------------------------------------
/docs/sample-template.md:
--------------------------------------------------------------------------------
1 | # example template for the template cache strategy:
2 |
3 | ```js
4 | var props = {
5 | foo: {
6 | bar: {
7 | a: [0, 1, 2, 3, 4],
8 | b: "hello"
9 | }
10 | }
11 | };
12 |
13 | var template = {
14 | foo: {
15 | bar: {
16 | a: [`@'0"@`, `@'1"@`, `@'2"@`, `@'3"@`, `@'4"@`],
17 | b: `@'5"@`
18 | }
19 | }
20 | };
21 |
22 | var lookup = {
23 | "@0@": "foo.bar.a.0",
24 | "@1@": "foo.bar.a.1",
25 | "@2@": "foo.bar.a.2",
26 | "@3@": "foo.bar.a.3",
27 | "@4@": "foo.bar.a.4",
28 | "@5@": "foo.bar.b"
29 | };
30 | ```
31 |
32 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | require("electrode-archetype-njs-module-dev")();
--------------------------------------------------------------------------------
/lib/cache-store.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | /* eslint-disable */
3 | function CacheStore(cfg) {
4 | this.cache = {};
5 | this.size = 0;
6 | this.entries = 0;
7 | this.config = cfg;
8 | }
9 |
10 | CacheStore.prototype.cleanCache = function (minFreeSize) {
11 | const keys = Object.keys(this.cache);
12 | keys.sort((a, b) => this.cache[a].access - this.cache[b].access);
13 | let freed = 0;
14 | for (let i = 0; i < keys.length; i++) {
15 | const key = keys[i];
16 | const entry = this.cache[key];
17 | if (freed >= minFreeSize) {
18 | break;
19 | }
20 | delete this.cache[key];
21 | const freeSize = key.length + entry.html.length;
22 | freed += freeSize;
23 | this.size -= freeSize;
24 | this.entries--;
25 | }
26 | };
27 |
28 | CacheStore.prototype.newEntry = function (name, key, value) {
29 | const entryKey = `${name}-${key}`;
30 | const size = entryKey.length + value.html.length;
31 | var newSize = this.size + size;
32 | if (newSize > this.config.MAX_CACHE_SIZE) {
33 | const freeSize = Math.max(size, this.config.minFreeCacheSize);
34 | this.cleanCache(Math.min(freeSize, this.config.maxFreeCacheSize));
35 | newSize = this.size + size;
36 | }
37 | this.cache[entryKey] = value;
38 | value.hits = 0;
39 | value.access = Date.now();
40 | this.size = newSize;
41 | this.entries++;
42 | };
43 |
44 | CacheStore.prototype.getEntry = function (name, key) {
45 | const entryKey = `${name}-${key}`;
46 | const x = this.cache[entryKey];
47 | if (x) {
48 | x.hits++;
49 | x.access = Date.now();
50 | }
51 | return x;
52 | };
53 |
54 | module.exports = CacheStore;
55 |
--------------------------------------------------------------------------------
/lib/config.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | /* eslint-disable no-magic-numbers */
4 |
5 | const config = {
6 | enabled: false,
7 | profiling: false,
8 | caching: false,
9 | debug: false,
10 | hashKey: true,
11 | stripUrlProtocol: true,
12 | cacheExpireTime: 15 * 60 * 1000, // 15 min
13 | MAX_CACHE_SIZE: 50 * 1024 * 1024, // 50Meg
14 | minFreeCacheSize: 1024 * 1024, // 1 Meg - min size to free when cache is full
15 | maxFreeCacheSize: 10 * 1024 * 1024 // 10 Meg - max size to free when cache is full
16 | };
17 |
18 | module.exports = config;
19 |
--------------------------------------------------------------------------------
/lib/ssr-caching.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | /* eslint-disable no-magic-numbers */
4 |
5 | const ReactCompositeComponent = require("react-dom/lib/ReactCompositeComponent");
6 | const DOMPropertyOperations = require("react-dom/lib/DOMPropertyOperations");
7 | const assert = require("assert");
8 | const _ = require("lodash");
9 | const CacheStore = require("./cache-store");
10 | const config = require("./config");
11 |
12 | const noOpHashKeyFn = (key) => key;
13 | let hashKeyFn = noOpHashKeyFn;
14 |
15 | exports.shouldHashKeys = function (flag, fn) {
16 | if (typeof flag === "boolean") {
17 | config.hashKey = flag;
18 | }
19 |
20 | if (config.hashKey) {
21 | if (!fn) {
22 | try {
23 | const FarmHash = require("farmhash"); // eslint-disable-line
24 | hashKeyFn = (s) => FarmHash.hash64(s);
25 | } catch (e) {
26 | console.log("farmhash module not available, turning off hashKey"); // eslint-disable-line
27 | config.hashKey = false;
28 | hashKeyFn = noOpHashKeyFn;
29 | }
30 | } else {
31 | assert(typeof fn === "function", "hashKey function is not a function");
32 | hashKeyFn = fn;
33 | }
34 | } else {
35 | hashKeyFn = noOpHashKeyFn;
36 | }
37 |
38 | exports.hashKeyFn = hashKeyFn;
39 | };
40 |
41 | exports.shouldHashKeys();
42 |
43 | const profileData = {};
44 | const blackListed = {};
45 | let cacheComponents = {};
46 | let cacheStore;
47 |
48 | //
49 | // cache key is generated from props
50 | // generate template from props for cache strategy template
51 | //
52 | // Note: It's hard and tricky to template non-string props. Since the code may
53 | // behave differently depending on a boolean being true/false, or a number with different
54 | // values. Even string props the code could behave differently base on what the value is.
55 | // For example, collection status could be "PUBLISHED", "UNPUBLISHED", etc.
56 | //
57 | // Non-string props could also be stringified differently by the component.
58 | //
59 | // returns { template, lookup, cacheKey }
60 | //
61 | function generateTemplate(props, opts) {
62 | const template = {};
63 | const lookup = {};
64 | const path = [];
65 | let index = 0;
66 | const cacheKey = [];
67 |
68 | const gen = (obj, tmpl) => { // eslint-disable-line
69 | const keys = Object.keys(obj);
70 | for (let i = 0; i < keys.length; i++) {
71 | const k = keys[i];
72 | const v = obj[k];
73 | if (opts.ignoreKeys.indexOf(k) >= 0) {
74 | tmpl[k] = v;
75 | continue;
76 | }
77 |
78 | cacheKey.push(k);
79 | //cacheKey.push(typeof v);
80 | if (opts.preserveKeys.indexOf(k) >= 0) {
81 | tmpl[k] = v;
82 | cacheKey.push(JSON.stringify(v));
83 | } else if (typeof v === "function") {
84 | tmpl[k] = v;
85 | } else if (v && typeof v === "object") {
86 | const isArray = Array.isArray(v);
87 | if (isArray) {
88 | tmpl[k] = [];
89 | cacheKey.push(`[${v.length}`);
90 | } else {
91 | tmpl[k] = {};
92 | }
93 | path.push(k);
94 | gen(v, tmpl[k]);
95 | isArray && cacheKey.push("]"); // eslint-disable-line
96 | path.pop(k);
97 | } else if (typeof v === "string") {
98 | if (!v && opts.preserveEmptyKeys.indexOf(k) >= 0) {
99 | // Sometimes components have logic dependent on strings being empty
100 | // For example: It skips showing the UI to display a message if it's empty
101 | tmpl[k] = v;
102 | } else {
103 | const templateValue = `@'${index}"@`;
104 | if (config.stripUrlProtocol) {
105 | const lv = v.toLowerCase();
106 | if (lv.startsWith("http://")) { // eslint-disable-line
107 | tmpl[k] = `http://${templateValue}`;
108 | } else if (lv.startsWith("https://")) {
109 | tmpl[k] = `https://${templateValue}`;
110 | } else {
111 | tmpl[k] = templateValue;
112 | }
113 | } else {
114 | tmpl[k] = templateValue;
115 | }
116 | const lookupKey = `@${index}@`;
117 | lookup[lookupKey] = path.concat(k);
118 | cacheKey.push(`:${lookupKey}`);
119 | index++;
120 | }
121 | } else if (v && opts.whiteListNonStringKeys.indexOf(k) >= 0) {
122 | tmpl[k] = `@'${index}"@`;
123 | const lookupKey = `@${index}@`;
124 | cacheKey.push(`:${lookupKey}`);
125 | lookup[lookupKey] = path.concat(k);
126 | index++;
127 | } else {
128 | tmpl[k] = v;
129 | cacheKey.push(JSON.stringify(v));
130 | }
131 | }
132 | };
133 |
134 | gen(props, template);
135 | return {
136 | template, lookup, cacheKey: cacheKey.join(",")
137 | };
138 | }
139 |
140 | const replacements = {
141 | "&": "&",
142 | "<": "<",
143 | ">": ">",
144 | "'": "'",
145 | '"': """ // eslint-disable-line
146 | };
147 |
148 | ReactCompositeComponent._construct = ReactCompositeComponent.construct;
149 | ReactCompositeComponent._mountComponent = ReactCompositeComponent.mountComponent;
150 |
151 | ReactCompositeComponent.construct = function (element) {
152 | if (config.enabled) {
153 | if (config.profiling) {
154 | this.__p = {time: undefined};
155 | this.__profileTime = this.__realProfileTime;
156 | }
157 | this._name = element.type && typeof element.type !== "string" && element.type.name;
158 | this.mountComponent = this._name ? this.mountComponentCache : this._mountComponent;
159 | }
160 | return this._construct(element);
161 | };
162 |
163 | ReactCompositeComponent.__profileTime = function () {
164 | };
165 |
166 | ReactCompositeComponent.__realProfileTime = function (start) {
167 | const name = this._name;
168 | const d = process.hrtime(start);
169 | const owner = this._currentElement._owner;
170 |
171 | if (owner) {
172 | (owner.__p[name] || (owner.__p[name] = [])).push(this.__p);
173 | } else {
174 | (profileData[name] || (profileData[name] = [])).push(this.__p);
175 | }
176 |
177 | assert(this.__p.time === undefined);
178 |
179 | this.__p.time = d[0] * 1000.0 + d[1] / 1000000.0;
180 | };
181 |
182 | const restoreRealProps = (r, lookup, realProps) => {
183 | return r.replace(/(\@\'|\@')([0-9]+)(\"\@|\"\@)/g, (m, a, b) => {
184 | let v = _.get(realProps, lookup[`@${b}@`]);
185 | if (typeof v === "string") {
186 | if (config.stripUrlProtocol) {
187 | const lv = v.toLowerCase();
188 | if (lv.startsWith("http://")) {
189 | v = v.substr(7);
190 | } else if (lv.startsWith("https://")) {
191 | v = v.substr(8);
192 | }
193 | }
194 | if (a === `@'`) {
195 | return v;
196 | }
197 | return v.replace(/([&<'">])/g, (m2, c2) => {
198 | return replacements[c2];
199 | });
200 | } else {
201 | return v;
202 | }
203 | });
204 | };
205 |
206 | const MARKUP_FOR_ROOT = DOMPropertyOperations.createMarkupForRoot();
207 |
208 | const addRootMarkup = (r) => {
209 | return r.replace(/(<[^ >]*)([ >])/, (m, a, b) => `${a} ${MARKUP_FOR_ROOT}${b}`);
210 | };
211 |
212 | const REACT_ID_REPLACE_REGEX = new RegExp(`(data-reactid="|react-text: |react-empty: )[0-9]*`, "g");
213 |
214 | const updateReactId = (r, hostContainerInfo) => { // eslint-disable-line
215 | let id = hostContainerInfo._idCounter;
216 | r = r.replace(REACT_ID_REPLACE_REGEX, (m, a) => `${a}${id++}`);
217 | hostContainerInfo._idCounter = id;
218 | return r;
219 | };
220 |
221 | const templatePostProcess = (r, lookup, realProps, transaction, hostContainerInfo) => { // eslint-disable-line
222 | r = restoreRealProps(r, lookup, realProps);
223 | return transaction.renderToStaticMarkup ? r : updateReactId(r, hostContainerInfo);
224 | };
225 |
226 | ReactCompositeComponent.mountComponentCache = function mountComponentCache(transaction, hostParent, hostContainerInfo, context, parentDebugID) { // eslint-disable-line
227 |
228 | let template;
229 | let cached;
230 | let key;
231 | let cacheType;
232 | let opts;
233 |
234 | const currentElement = Object.assign({}, this._currentElement);
235 | this._currentElement = currentElement;
236 | const saveProps = currentElement.props;
237 | const name = this._name;
238 | const startTime = config.profiling && process.hrtime();
239 |
240 | // if props has children then can't cache
241 | // if owner is already caching, then no need to cache
242 | const canCache = (opts = cacheComponents[name]) && opts.enable && !(transaction._cached ||
243 | _.isEmpty(saveProps) || typeof saveProps.children === "object");
244 |
245 | const doCache = config.caching && canCache;
246 |
247 | if (doCache) {
248 | const strategy = opts.strategy;
249 | if (strategy === "simple") {
250 | cacheType = "cache";
251 | template = {
252 | cacheKey: opts.genCacheKey ? opts.genCacheKey(saveProps) : JSON.stringify(saveProps)
253 | };
254 | key = hashKeyFn(template.cacheKey);
255 | cached = cacheStore.getEntry(name, key);
256 | if (cached) {
257 | this.__profileTime(startTime);
258 | let r = cached.html;
259 | if (!transaction.renderToStaticMarkup) {
260 | r = updateReactId(cached.html, hostContainerInfo);
261 | }
262 | if (!hostParent) {
263 | r = addRootMarkup(r);
264 | }
265 | return config.debug ?
266 | `${r}` : r;
267 | }
268 | } else if (strategy === "template") {
269 | cacheType = "cache";
270 | template = generateTemplate(saveProps, opts);
271 | key = hashKeyFn(template.cacheKey);
272 | cached = cacheStore.getEntry(name, key);
273 | if (cached) {
274 | let r = templatePostProcess(cached.html, template.lookup,
275 | saveProps, transaction, hostContainerInfo);
276 | if (!hostParent) {
277 | r = addRootMarkup(r);
278 | }
279 | this.__profileTime(startTime);
280 | return config.debug ? `${r}` : r;
281 | }
282 | } else {
283 | throw new Error(`Unknown caching strategy ${strategy} for component ${name}`);
284 | }
285 | } else if (transaction._cached) {
286 | cacheType = "byParent";
287 | } else {
288 | cacheType = "NONE";
289 | }
290 |
291 | const saveId = hostContainerInfo._idCounter;
292 |
293 | if (template) {
294 | if (template.template) {
295 | currentElement.props = template.template;
296 | }
297 | hostContainerInfo._idCounter = 1;
298 | transaction._cached = this._name;
299 | }
300 |
301 | let r = this._mountComponent(transaction, hostParent, hostContainerInfo, context, parentDebugID);
302 |
303 | if (template) {
304 | hostContainerInfo._idCounter = saveId;
305 | currentElement.props = saveProps;
306 | cacheStore.newEntry(name, key, {html: r.replace(` ${MARKUP_FOR_ROOT}`, "")});
307 |
308 | if (template.lookup) {
309 | r = templatePostProcess(r, template.lookup,
310 | saveProps, transaction, hostContainerInfo);
311 | } else if (!transaction.renderToStaticMarkup) {
312 | r = updateReactId(r, hostContainerInfo);
313 | }
314 | transaction._cached = undefined;
315 | }
316 |
317 | this.__profileTime(startTime);
318 |
319 | if (config.caching && config.debug) {
320 | return `${r}`;
321 | } else {
322 | return r;
323 | }
324 | };
325 |
326 | exports.enableProfiling = function (flag) {
327 | config.profiling = flag === undefined || !!flag;
328 | config.enabled = config.profiling || config.caching;
329 | };
330 |
331 | exports.enableCaching = function (flag) {
332 | config.caching = flag === undefined || !!flag;
333 | config.enabled = config.profiling || config.caching;
334 | };
335 |
336 | exports.enableCachingDebug = function (flag) {
337 | config.debug = flag === undefined || !!flag;
338 | };
339 |
340 | exports.stripUrlProtocol = function (flag) {
341 | config.stripUrlProtocol = flag;
342 | };
343 |
344 | exports.profileData = profileData;
345 |
346 | exports.clearProfileData = function () {
347 | Object.keys(profileData).forEach((k) => {
348 | delete profileData[k];
349 | });
350 | };
351 |
352 | exports.clearCache = function () {
353 | exports.cacheStore = cacheStore = new CacheStore(config);
354 | };
355 |
356 | exports.cacheSize = function () {
357 | return cacheStore.size;
358 | };
359 |
360 | exports.cacheEntries = function () {
361 | return cacheStore.entries;
362 | };
363 |
364 | exports.cacheHitReport = function () {
365 | const hitReport = {};
366 | Object.keys(cacheStore.cache).forEach((key) => {
367 | const entry = cacheStore.cache[key];
368 | hitReport[key] = {
369 | hits: entry.hits
370 | };
371 | });
372 | return hitReport;
373 | };
374 |
375 | exports.config = config;
376 |
377 | exports.setCachingConfig = function (cfg) {
378 | cacheComponents = cfg.components || {};
379 | Object.keys(cacheComponents).forEach((k) => {
380 | _.defaults(cacheComponents[k], {
381 | preserveKeys: [],
382 | preserveEmptyKeys: [],
383 | ignoreKeys: [],
384 | whiteListNonStringKeys: []
385 | });
386 | });
387 | };
388 |
389 | exports.blackListed = blackListed;
390 |
391 | exports.clearCache();
392 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "electrode-react-ssr-caching",
3 | "version": "0.1.6",
4 | "description": "Optimize React SSR with profiling and component caching",
5 | "main": "lib/ssr-caching.js",
6 | "scripts": {
7 | "test": "npm run build-test && gulp check",
8 | "build-test": "rm -f test/gen-lib/* && babel test/src -d test/gen-lib",
9 | "coverage": "npm run build-test && gulp test-cov"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "git@github.com:electrode-io/electrode-react-ssr-caching.git"
14 | },
15 | "keywords": [],
16 | "author": "Joel Chen ",
17 | "license": "Apache-2.0",
18 | "optionalDependencies": {
19 | "farmhash": "^1.1.1"
20 | },
21 | "peerDependencies": {
22 | "react": "^15.3.0"
23 | },
24 | "devDependencies": {
25 | "babel-cli": "^6.10.1",
26 | "babel-core": "^6.10.4",
27 | "babel-preset-es2015": "^6.9.0",
28 | "babel-preset-es2015-loose": "^7.0.0",
29 | "babel-preset-react": "^6.11.1",
30 | "babel-register": "^6.9.0",
31 | "electrode-archetype-njs-module-dev": "^1.0.1",
32 | "gulp": "^3.9.1",
33 | "mock-require": "^1.3.0",
34 | "react": "^15.3.0",
35 | "react-dom": "^15.3.0",
36 | "react-redux": "^4.4.5",
37 | "redux": "^3.5.2"
38 | },
39 | "dependencies": {
40 | "lodash": "^4.13.1"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/test/.eslintrc:
--------------------------------------------------------------------------------
1 | ---
2 | extends:
3 | - "../node_modules/electrode-archetype-njs-module-dev/config/eslint/.eslintrc-test"
4 |
--------------------------------------------------------------------------------
/test/farmhash-mock.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const mock = require("mock-require");
4 | const crypto = require("crypto");
5 |
6 | mock("farmhash", {
7 | hash64: function (s) {
8 | const hash = crypto.createHmac("md5", "")
9 | .update(s)
10 | .digest("hex");
11 |
12 | return hash;
13 | }
14 | });
15 |
--------------------------------------------------------------------------------
/test/spec/cache-store.spec.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | // test cache store feature
4 |
5 | const CacheStore = require("../../lib/cache-store");
6 |
7 | describe("CacheStore", function () {
8 | it("should cache entry", function () {
9 | const cacheStore = new CacheStore({
10 | cacheExpireTime: 200,
11 | MAX_CACHE_SIZE: 1024,
12 | minFreeCacheSize: 200,
13 | maxFreeCacheSize: 400
14 | });
15 | expect(cacheStore.getEntry("test", "1")).to.equal(undefined);
16 | cacheStore.newEntry("test", "1", {html: "hello"});
17 | expect(cacheStore.getEntry("test", "1")).to.be.ok;
18 | expect(cacheStore.getEntry("test", "1").html).to.equal("hello");
19 | expect(cacheStore.getEntry("test", "1").hits).to.equal(3);
20 | });
21 |
22 | it("should free up cache", function (done) {
23 | const cacheStore = new CacheStore({
24 | cacheExpireTime: 100,
25 | MAX_CACHE_SIZE: 85,
26 | minFreeCacheSize: 20,
27 | maxFreeCacheSize: 40
28 | });
29 | cacheStore.newEntry("test", "1", {html: "hello1"});
30 | cacheStore.newEntry("test", "2", {html: "hello2"});
31 | cacheStore.newEntry("test", "3", {html: "hello3"});
32 | cacheStore.newEntry("test", "4", {html: "hello4"});
33 | cacheStore.newEntry("test", "5", {html: "hello5"});
34 | cacheStore.newEntry("test", "6", {html: "hello6"});
35 | cacheStore.newEntry("test", "7", {html: "hello7"});
36 | setTimeout(() => {
37 | cacheStore.getEntry("test", "5");
38 | cacheStore.newEntry("foobar", "1", {html: "blahblahblahblahblah"});
39 | expect(Object.keys(cacheStore.cache)).includes("test-4", "test-6", "test-5", "foobar-1");
40 | cacheStore.newEntry("foobar", "2", {html: "blahblahblahblahblah"});
41 | expect(Object.keys(cacheStore.cache)).to.deep.equal(["test-5", "foobar-1", "foobar-2"]);
42 | done();
43 | }, 90);
44 | });
45 | });
46 |
--------------------------------------------------------------------------------
/test/spec/caching.simple.spec.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | // test simple caching feature
4 |
5 | require("../farmhash-mock");
6 | const SSRCaching = require("../..");
7 | const renderGreeting = require("../gen-lib/render-greeting").default;
8 | const chai = require("chai");
9 | const expect = chai.expect;
10 | process.env.NODE_ENV = "production";
11 |
12 | describe("SSRCaching simple caching", function () {
13 | afterEach(() => {
14 | SSRCaching.setCachingConfig({});
15 | SSRCaching.clearCache();
16 | SSRCaching.clearProfileData();
17 | });
18 |
19 | const verifyRenderResults = (r1, r2, r3) => {
20 | expect(r1).to.equal(r2);
21 | expect(r1).to.equal(r3);
22 | expect(r2).to.equal(r3);
23 | };
24 |
25 | //
26 | // test simple strategy with user provided function to generate cache key
27 | //
28 | it("should cache component with simple strategy", function () {
29 | const message = "how're you?";
30 |
31 | let start = Date.now();
32 | const r1 = renderGreeting("test", message);
33 | const r1Time = Date.now() - start;
34 |
35 | SSRCaching.enableCaching();
36 | SSRCaching.setCachingConfig({
37 | components: {
38 | "Hello": {
39 | strategy: "simple",
40 | enable: true,
41 | genCacheKey: () => "key-simple"
42 | }
43 | }
44 | });
45 |
46 | // should add an entry to cache with key-simple
47 |
48 | SSRCaching.shouldHashKeys(false);
49 | renderGreeting("test", message);
50 | expect(SSRCaching.cacheStore.getEntry("Hello", "key-simple").hits).to.equal(1);
51 |
52 | // should add an entry to cache with hashed key from key-simple
53 |
54 | SSRCaching.shouldHashKeys(true);
55 | start = Date.now();
56 | const r2 = renderGreeting("test", message);
57 | const r2Time = Date.now() - start;
58 | const entry = SSRCaching.cacheStore.getEntry("Hello", "500034349202595839ffe2cb6f83665b");
59 | expect(entry.hits).to.equal(1);
60 |
61 | // now render should use result from cache
62 |
63 | start = Date.now();
64 | const r3 = renderGreeting("test", message);
65 | const r3Time = Date.now() - start;
66 |
67 | console.log(`rendering time r1 ${r1Time}ms r2 ${r2Time} r3 (cached) ${r3Time}`);
68 | expect(r3Time).below(r1Time);
69 | expect(r3Time).below(r2Time);
70 |
71 | expect(entry.hits).to.equal(2);
72 | verifyRenderResults(r1, r2, r3);
73 | });
74 |
75 | //
76 | // test simple strategy with JSON.stringify on props to generate cache key
77 | //
78 | it("should cache component with simple strategy and stringify", function () {
79 | const message = "good morning";
80 |
81 | SSRCaching.enableProfiling(true);
82 | const r1 = renderGreeting("test", message);
83 | const data = SSRCaching.profileData;
84 | expect(data.Greeting[0].Hello[0].time).to.be.above(0);
85 |
86 | SSRCaching.enableProfiling(false);
87 | SSRCaching.clearProfileData();
88 | expect(data).to.deep.equal({});
89 |
90 | SSRCaching.enableCaching();
91 | SSRCaching.setCachingConfig({
92 | components: {
93 | "Hello": {
94 | strategy: "simple",
95 | enable: true
96 | }
97 | }
98 | });
99 |
100 | // should add an entry to cache with stringified props as cache key
101 |
102 | SSRCaching.shouldHashKeys(false);
103 | renderGreeting("test", message);
104 | expect(SSRCaching.cacheStore.getEntry("Hello", JSON.stringify({name: "test", message})).hits).to.equal(1);
105 |
106 | // should add an entry to cache with hashed value of key
107 |
108 | SSRCaching.shouldHashKeys(true);
109 | const r2 = renderGreeting("test", message);
110 | const entry = SSRCaching.cacheStore.getEntry("Hello", "6a86523415a68c4c7580fe6db324923c");
111 | expect(entry.hits).to.equal(1);
112 |
113 | // now render should use result from cache
114 |
115 | SSRCaching.enableProfiling(true);
116 | const r3 = renderGreeting("test", message);
117 | expect(data.Greeting[0].Hello[0].time).to.be.above(0);
118 | expect(entry.hits).to.equal(2);
119 | verifyRenderResults(r1, r2, r3);
120 | });
121 | });
122 |
--------------------------------------------------------------------------------
/test/spec/caching.template.spec.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | // test template caching feature
4 |
5 | require("../farmhash-mock");
6 | const SSRCaching = require("../..");
7 | const renderGreeting = require("../gen-lib/render-greeting").default;
8 | const renderBoard = require("../gen-lib/render-board").default;
9 | const renderHello = require("../gen-lib/render-hello").default;
10 | const chai = require("chai");
11 | const expect = chai.expect;
12 | process.env.NODE_ENV = "production";
13 |
14 | describe("SSRCaching template caching", function () {
15 |
16 | this.timeout(10000);
17 |
18 | beforeEach(() => {
19 | SSRCaching.setCachingConfig({});
20 | SSRCaching.clearCache();
21 | SSRCaching.clearProfileData();
22 | SSRCaching.enableCaching(false);
23 | SSRCaching.enableProfiling(false);
24 | });
25 |
26 | function removeReactChecksum(html) {
27 | return html.replace(/data-react-checksum\=\"[^\"]+\"/g, "").replace(/ *>/g, ">");
28 | }
29 |
30 | const verifyRenderResults = (r1, r2, r3, cleanUp) => { // eslint-disable-line
31 | if (cleanUp) {
32 | r1 = removeReactChecksum(r1);
33 | r2 = removeReactChecksum(r2);
34 | r3 = removeReactChecksum(r3);
35 | }
36 | expect(r1).to.equal(r2);
37 | expect(r1).to.equal(r3);
38 | expect(r2).to.equal(r3);
39 | };
40 |
41 | //
42 | // test simple strategy with user provided function to generate cache key
43 | //
44 | it("should cache component with template strategy", function () {
45 | const message = "good morning";
46 |
47 | // save render Hello with caching off
48 | const rHello1 = renderHello("test", message); // eslint-disable-line
49 |
50 | // save render Greeting with caching off
51 | const r1 = renderGreeting("test", message);
52 |
53 | // Enable caching and test
54 |
55 | SSRCaching.enableCaching();
56 | SSRCaching.setCachingConfig({
57 | components: {
58 | "Hello": {
59 | strategy: "template",
60 | enable: true
61 | }
62 | }
63 | });
64 |
65 | // should add an entry to cache with template key
66 |
67 | SSRCaching.stripUrlProtocol(true);
68 | SSRCaching.shouldHashKeys(false);
69 |
70 | // first just render Hello by itself to create a cache with diff react-id's
71 |
72 | renderHello("test", message); // eslint-disable-line
73 | const key1 = Object.keys(SSRCaching.cacheStore.cache)[0];
74 | const keyTmpl = key1.substr(6);
75 | const entry1 = SSRCaching.cacheStore.getEntry("Hello", keyTmpl);
76 | expect(entry1.hits).to.equal(1);
77 |
78 | // render hello again and verify cache
79 |
80 | const rHello2 = renderHello("test", message);
81 | expect(rHello1).to.equal(rHello2);
82 | expect(entry1.hits).to.equal(2);
83 |
84 | // render Greeting that has Hello inside and verify
85 |
86 | const rX = renderGreeting("test", message);
87 | expect(entry1.hits).to.equal(3);
88 | expect(r1).to.equal(rX);
89 |
90 | // should add an entry to cache with hashed key from template key
91 |
92 | SSRCaching.shouldHashKeys(true);
93 | const hashKey = SSRCaching.hashKeyFn(keyTmpl);
94 | const r2 = renderGreeting("test", message);
95 | const entry = SSRCaching.cacheStore.getEntry("Hello", hashKey);
96 | expect(entry.hits).to.equal(1);
97 |
98 | // now render should use result from cache
99 |
100 | const r3 = renderGreeting("test", message);
101 | expect(entry.hits).to.equal(2);
102 | expect(r2).includes(message);
103 | verifyRenderResults(r1, r2, r3);
104 |
105 | const hitReport = SSRCaching.cacheHitReport();
106 | Object.keys(hitReport).forEach((key) => {
107 | console.log(`Cache Entry ${key} Hits ${hitReport[key].hits}`); // eslint-disable-line
108 | });
109 |
110 | expect(SSRCaching.cacheEntries()).to.equal(2);
111 | expect(SSRCaching.cacheSize()).to.be.above(0);
112 | });
113 |
114 | const users = [
115 | {
116 | name: "Joel",
117 | message: "good morning",
118 | hasAvatar: true,
119 | quote: "blah",
120 | preserve: "preserve",
121 | empty: "",
122 | ignore: "ignore-me",
123 | urls: [
124 | "http://zzzzl.com/xchen11",
125 | "https://github.com/jchip"
126 | ],
127 | data: {
128 | location: "SR",
129 | role: "dev",
130 | distance: 40
131 | },
132 | random: 1,
133 | f: () => 0
134 | },
135 | {
136 | name: "Alex",
137 | message: "how're you?",
138 | hasAvatar: false,
139 | quote: "let's do this",
140 | preserve: "preserve",
141 | urls: [
142 | "http://zzzzl.com/ag",
143 | "https://github.com/alex"
144 | ],
145 | data: {
146 | location: "SJ",
147 | role: "dir",
148 | distance: 20
149 | },
150 | random: 2,
151 | f: () => 0
152 | },
153 | {
154 | name: "Arpan",
155 | message: "how's your kitchen?",
156 | hasAvatar: true,
157 | quote: "what's up?",
158 | preserve: "preserve",
159 | urls: [
160 | "http://zzzzl.com/aa",
161 | "https://github.com/arpan"
162 | ],
163 | data: {
164 | location: "CV",
165 | role: "dev",
166 | distance: 30
167 | },
168 | random: 3,
169 | f: () => 0
170 | }
171 | ];
172 |
173 | const cacheConfig = {
174 | components: {
175 | "Heading": {
176 | strategy: "simple",
177 | enable: true
178 | },
179 | "Hello": {
180 | strategy: "simple",
181 | enable: true
182 | },
183 | "Board": {
184 | strategy: "template",
185 | enable: false
186 | },
187 | "InfoCard": {
188 | strategy: "template",
189 | enable: true,
190 | preserveKeys: ["preserve"],
191 | preserveEmptyKeys: ["empty"],
192 | ignoreKeys: ["ignore"],
193 | whiteListNonStringKeys: ["random", "distance"]
194 | }
195 | }
196 | };
197 |
198 | const verifyAndRemoveUrlProtocol = (r) => {
199 | expect(r).includes("http://");
200 | expect(r).includes("https://");
201 | r = r.replace(/http:/g, "");
202 | r = r.replace(/https:/g, "");
203 | return r;
204 | };
205 |
206 | const testTemplate = (stripUrlProtocol, hashKeys, profiling) => {
207 | SSRCaching.enableProfiling(profiling);
208 |
209 | let start = Date.now();
210 | const r1 = renderBoard(users);
211 | const r1Time = Date.now() - start;
212 |
213 | if (profiling) {
214 | const data = SSRCaching.profileData;
215 | expect(data.Board[0].InfoCard[0].time).to.be.above(0);
216 | SSRCaching.clearProfileData();
217 | SSRCaching.enableProfiling(false);
218 | }
219 |
220 | SSRCaching.enableCaching();
221 | SSRCaching.setCachingConfig(cacheConfig);
222 | SSRCaching.shouldHashKeys(hashKeys);
223 | SSRCaching.stripUrlProtocol(stripUrlProtocol);
224 |
225 | start = Date.now();
226 | let r2 = renderBoard(users);
227 | const r2Time = Date.now() - start;
228 |
229 | const cache = SSRCaching.cacheStore.cache;
230 | const keys = Object.keys(cache);
231 | keys.forEach((x) => {
232 | expect(cache[x].hits).to.equal(0);
233 | });
234 |
235 | SSRCaching.enableProfiling(profiling);
236 |
237 | start = Date.now();
238 | let r3 = renderBoard(users);
239 | const r3Time = Date.now() - start;
240 |
241 | if (profiling) {
242 | const data = SSRCaching.profileData;
243 | expect(data.Board[0].InfoCard[0].time).to.be.above(0);
244 | SSRCaching.clearProfileData();
245 | }
246 |
247 | console.log(`rendering time r1 ${r1Time}ms r2 ${r2Time}ms r3 (cached) ${r3Time}ms`);
248 | expect(r3Time).below(r1Time);
249 | expect(r3Time).below(r2Time);
250 |
251 | expect(Object.keys(cache)).to.deep.equal(keys);
252 |
253 | keys.forEach((x) => {
254 | expect(cache[x].hits).to.equal(1);
255 | if (!hashKeys) {
256 | expect(x).not.includes("ignore");
257 | }
258 | });
259 |
260 | if (!stripUrlProtocol) {
261 | r2 = verifyAndRemoveUrlProtocol(r2);
262 | r3 = verifyAndRemoveUrlProtocol(r3);
263 | }
264 |
265 | verifyRenderResults(r1, r2, r3, !stripUrlProtocol);
266 | };
267 |
268 | it("should handle other scenarios for template cache", function () {
269 | testTemplate(false, false, false);
270 | });
271 |
272 | it("should handle template and strip url protocol", function () {
273 | testTemplate(true, true, true);
274 | });
275 |
276 | it("should support debug caching", function () {
277 | renderBoard(users);
278 | SSRCaching.enableCaching();
279 | SSRCaching.enableCachingDebug();
280 | expect(SSRCaching.config.debug).to.equal(true);
281 | SSRCaching.enableCachingDebug(false);
282 | expect(SSRCaching.config.debug).to.equal(false);
283 | SSRCaching.enableCachingDebug(true);
284 | expect(SSRCaching.config.debug).to.equal(true);
285 | SSRCaching.setCachingConfig(cacheConfig);
286 | SSRCaching.shouldHashKeys(false);
287 | const r2 = renderBoard(users);
288 | expect(r2).includes("