Skip to content

Commit 20123a4

Browse files
committed
Add fixture for comparing baseline render perf for renderToString and renderToPipeableStream
Modified from ssr2 and https://github.com/SuperOleg39/react-ssr-perf-test
1 parent 7ed1173 commit 20123a4

14 files changed

+5913
-0
lines changed

fixtures/fizz/README.md

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Fizz Fixtures
2+
3+
A set of basic tests for Fizz primarily focussed on baseline perfomrance of legacy renderToString and streaming implementations.
4+
5+
## Setup
6+
7+
To reference a local build of React, first run `npm run build` at the root
8+
of the React project. Then:
9+
10+
```
11+
cd fixtures/fizz
12+
yarn
13+
yarn start
14+
```
15+
16+
The `start` command runs a webpack dev server and a server-side rendering server in development mode with hot reloading.
17+
18+
**Note: whenever you make changes to React and rebuild it, you need to re-run `yarn` in this folder:**
19+
20+
```
21+
yarn
22+
```
23+
24+
If you want to try the production mode instead run:
25+
26+
```
27+
yarn start:prod
28+
```
29+
30+
This will pre-build all static resources and then start a server-side rendering HTTP server that hosts the React app and service the static resources (without hot reloading).

fixtures/fizz/package.json

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
{
2+
"name": "react-ssr",
3+
"version": "0.1.0",
4+
"private": true,
5+
"engines": {
6+
"node": ">=14.9.0"
7+
},
8+
"license": "MIT",
9+
"dependencies": {
10+
"@babel/core": "7.14.3",
11+
"@babel/register": "7.13.16",
12+
"babel-loader": "8.1.0",
13+
"babel-preset-react-app": "10.0.0",
14+
"compression": "^1.7.4",
15+
"concurrently": "^5.3.0",
16+
"express": "^4.17.1",
17+
"nodemon": "^2.0.6",
18+
"react": "link:../../build/node_modules/react",
19+
"react-dom": "link:../../build/node_modules/react-dom",
20+
"react-error-boundary": "^3.1.3",
21+
"resolve": "1.12.0",
22+
"rimraf": "^3.0.2",
23+
"webpack": "4.44.2",
24+
"webpack-cli": "^4.2.0"
25+
},
26+
"devDependencies": {
27+
"cross-env": "^7.0.3",
28+
"prettier": "1.19.1"
29+
},
30+
"scripts": {
31+
"start": "concurrently \"npm run server:dev\" \"npm run bundler:dev\"",
32+
"start:prod": "concurrently \"npm run server:prod\" \"npm run bundler:prod\"",
33+
"server:dev": "cross-env NODE_ENV=development nodemon -- --inspect server/server.js",
34+
"server:prod": "cross-env NODE_ENV=production nodemon -- server/server.js",
35+
"bundler:dev": "cross-env NODE_ENV=development nodemon -- scripts/build.js",
36+
"bundler:prod": "cross-env NODE_ENV=production nodemon -- scripts/build.js"
37+
},
38+
"babel": {
39+
"presets": [
40+
[
41+
"react-app",
42+
{
43+
"runtime": "automatic"
44+
}
45+
]
46+
]
47+
},
48+
"nodemonConfig": {
49+
"ignore": [
50+
"build/*"
51+
]
52+
}
53+
}

fixtures/fizz/public/main.css

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
body {
2+
font-family: system-ui, sans-serif;
3+
}
4+
5+
* {
6+
box-sizing: border-box;
7+
}
8+
9+
nav {
10+
padding: 20px;
11+
}
12+
13+
.sidebar {
14+
padding: 10px;
15+
height: 500px;
16+
float: left;
17+
width: 30%;
18+
}
19+
20+
.post {
21+
padding: 20px;
22+
float: left;
23+
width: 60%;
24+
}
25+
26+
h1, h2 {
27+
padding: 0;
28+
}
29+
30+
ul, li {
31+
margin: 0;
32+
}
33+
34+
.post p {
35+
font-size: larger;
36+
font-family: Georgia, serif;
37+
}
38+
39+
.comments {
40+
margin-top: 40px;
41+
}
42+
43+
.comment {
44+
border: 2px solid #aaa;
45+
border-radius: 4px;
46+
padding: 20px;
47+
}
48+
49+
/* https://codepen.io/mandelid/pen/vwKoe */
50+
.spinner {
51+
display: inline-block;
52+
transition: opacity linear 0.1s;
53+
width: 20px;
54+
height: 20px;
55+
border: 3px solid rgba(80, 80, 80, 0.5);
56+
border-radius: 50%;
57+
border-top-color: #fff;
58+
animation: spin 1s ease-in-out infinite;
59+
opacity: 0;
60+
}
61+
.spinner--active {
62+
opacity: 1;
63+
}
64+
65+
@keyframes spin {
66+
to {
67+
transform: rotate(360deg);
68+
}
69+
}
70+
@keyframes spin {
71+
to {
72+
transform: rotate(360deg);
73+
}
74+
}

fixtures/fizz/scripts/build.js

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
*/
8+
9+
'use strict';
10+
11+
const path = require('path');
12+
const rimraf = require('rimraf');
13+
const webpack = require('webpack');
14+
15+
const isProduction = process.env.NODE_ENV === 'production';
16+
rimraf.sync(path.resolve(__dirname, '../build'));
17+
webpack(
18+
{
19+
mode: isProduction ? 'production' : 'development',
20+
devtool: isProduction ? 'source-map' : 'cheap-module-source-map',
21+
entry: [path.resolve(__dirname, '../src/index.js')],
22+
output: {
23+
path: path.resolve(__dirname, '../build'),
24+
filename: 'main.js',
25+
},
26+
module: {
27+
rules: [
28+
{
29+
test: /\.js$/,
30+
use: 'babel-loader',
31+
exclude: /node_modules/,
32+
},
33+
],
34+
},
35+
},
36+
(err, stats) => {
37+
if (err) {
38+
console.error(err.stack || err);
39+
if (err.details) {
40+
console.error(err.details);
41+
}
42+
process.exit(1);
43+
}
44+
const info = stats.toJson();
45+
if (stats.hasErrors()) {
46+
console.log('Finished running webpack with errors.');
47+
info.errors.forEach(e => console.error(e));
48+
process.exit(1);
49+
} else {
50+
console.log('Finished running webpack.');
51+
}
52+
}
53+
);

fixtures/fizz/server/delays.js

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
*/
8+
9+
// Tweak these to play with different kinds of latency.
10+
11+
// How long the data fetches on the server.
12+
exports.API_DELAY = 2000;
13+
14+
// How long the server waits for data before giving up.
15+
exports.ABORT_DELAY = 10000;
16+
17+
// How long serving the JS bundles is delayed.
18+
exports.JS_BUNDLE_DELAY = 4000;
+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
*/
8+
9+
import {Writable} from 'stream';
10+
import * as React from 'react';
11+
import {renderToPipeableStream} from 'react-dom/server';
12+
import App from '../src/App';
13+
import {ABORT_DELAY} from './delays';
14+
15+
// In a real setup, you'd read it from webpack build stats.
16+
let assets = {
17+
'main.js': '/main.js',
18+
'main.css': '/main.css',
19+
};
20+
21+
function HtmlWritable(options) {
22+
Writable.call(this, options);
23+
this.chunks = [];
24+
this.html = '';
25+
}
26+
27+
HtmlWritable.prototype = Object.create(Writable.prototype);
28+
HtmlWritable.prototype.getHtml = function getHtml() {
29+
return this.html;
30+
};
31+
HtmlWritable.prototype._write = function _write(chunk, encoding, callback) {
32+
this.chunks.push(chunk);
33+
callback();
34+
};
35+
HtmlWritable.prototype._final = function _final(callback) {
36+
this.html = Buffer.concat(this.chunks).toString();
37+
callback();
38+
};
39+
40+
module.exports = function render(url, res) {
41+
let writable = new HtmlWritable();
42+
res.socket.on('error', error => {
43+
console.error('Fatal', error);
44+
});
45+
let didError = false;
46+
let didFinish = false;
47+
48+
writable.on('finish', () => {
49+
// If something errored before we started streaming, we set the error code appropriately.
50+
res.statusCode = didError ? 500 : 200;
51+
res.setHeader('Content-type', 'text/html');
52+
res.send(writable.getHtml());
53+
});
54+
55+
const {pipe, abort} = renderToPipeableStream(<App assets={assets} />, {
56+
bootstrapScripts: [assets['main.js']],
57+
onAllReady() {
58+
// Full completion.
59+
// You can use this for SSG or crawlers.
60+
didFinish = true;
61+
},
62+
onShellReady() {
63+
// If something errored before we started streaming, we set the error code appropriately.
64+
pipe(writable);
65+
},
66+
onShellError(x) {
67+
// Something errored before we could complete the shell so we emit an alternative shell.
68+
res.statusCode = 500;
69+
res.send('<!doctype><p>Error</p>');
70+
},
71+
onError(x) {
72+
didError = true;
73+
console.error(x);
74+
},
75+
});
76+
// Abandon and switch to client rendering if enough time passes.
77+
// Try lowering this to see the client recover.
78+
setTimeout(() => {
79+
if (!didFinish) {
80+
abort();
81+
}
82+
}, ABORT_DELAY);
83+
};
+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
*/
8+
9+
import * as React from 'react';
10+
import {renderToPipeableStream} from 'react-dom/server';
11+
import App from '../src/App';
12+
import {ABORT_DELAY} from './delays';
13+
14+
// In a real setup, you'd read it from webpack build stats.
15+
let assets = {
16+
'main.js': '/main.js',
17+
'main.css': '/main.css',
18+
};
19+
20+
module.exports = function render(url, res) {
21+
// The new wiring is a bit more involved.
22+
res.socket.on('error', error => {
23+
console.error('Fatal', error);
24+
});
25+
let didError = false;
26+
let didFinish = false;
27+
const {pipe, abort} = renderToPipeableStream(<App assets={assets} />, {
28+
bootstrapScripts: [assets['main.js']],
29+
onAllReady() {
30+
// Full completion.
31+
// You can use this for SSG or crawlers.
32+
didFinish = true;
33+
},
34+
onShellReady() {
35+
// If something errored before we started streaming, we set the error code appropriately.
36+
res.statusCode = didError ? 500 : 200;
37+
res.setHeader('Content-type', 'text/html');
38+
pipe(res);
39+
},
40+
onShellError(x) {
41+
// Something errored before we could complete the shell so we emit an alternative shell.
42+
res.statusCode = 500;
43+
res.send('<!doctype><p>Error</p>');
44+
},
45+
onError(x) {
46+
didError = true;
47+
console.error(x);
48+
},
49+
});
50+
// Abandon and switch to client rendering if enough time passes.
51+
// Try lowering this to see the client recover.
52+
setTimeout(() => {
53+
if (!didFinish) {
54+
abort();
55+
}
56+
}, ABORT_DELAY);
57+
};

0 commit comments

Comments
 (0)