Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fizz] Pipeable Stream Perf #24291

Merged
merged 3 commits into from Apr 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
30 changes: 30 additions & 0 deletions fixtures/fizz/README.md
@@ -0,0 +1,30 @@
# Fizz Fixtures

A set of basic tests for Fizz primarily focussed on baseline perfomrance of legacy renderToString and streaming implementations.

## Setup

To reference a local build of React, first run `npm run build` at the root
of the React project. Then:

```
cd fixtures/fizz
yarn
yarn start
```

The `start` command runs a webpack dev server and a server-side rendering server in development mode with hot reloading.

**Note: whenever you make changes to React and rebuild it, you need to re-run `yarn` in this folder:**

```
yarn
```

If you want to try the production mode instead run:

```
yarn start:prod
```

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).
53 changes: 53 additions & 0 deletions fixtures/fizz/package.json
@@ -0,0 +1,53 @@
{
"name": "react-ssr",
"version": "0.1.0",
"private": true,
"engines": {
"node": ">=14.9.0"
},
"license": "MIT",
"dependencies": {
"@babel/core": "7.14.3",
"@babel/register": "7.13.16",
"babel-loader": "8.1.0",
"babel-preset-react-app": "10.0.0",
"compression": "^1.7.4",
"concurrently": "^5.3.0",
"express": "^4.17.1",
"nodemon": "^2.0.6",
"react": "link:../../build/node_modules/react",
"react-dom": "link:../../build/node_modules/react-dom",
"react-error-boundary": "^3.1.3",
"resolve": "1.12.0",
"rimraf": "^3.0.2",
"webpack": "4.44.2",
"webpack-cli": "^4.2.0"
},
"devDependencies": {
"cross-env": "^7.0.3",
"prettier": "1.19.1"
},
"scripts": {
"start": "concurrently \"npm run server:dev\" \"npm run bundler:dev\"",
"start:prod": "concurrently \"npm run server:prod\" \"npm run bundler:prod\"",
"server:dev": "cross-env NODE_ENV=development nodemon -- --inspect server/server.js",
"server:prod": "cross-env NODE_ENV=production nodemon -- server/server.js",
"bundler:dev": "cross-env NODE_ENV=development nodemon -- scripts/build.js",
"bundler:prod": "cross-env NODE_ENV=production nodemon -- scripts/build.js"
},
"babel": {
"presets": [
[
"react-app",
{
"runtime": "automatic"
}
]
]
},
"nodemonConfig": {
"ignore": [
"build/*"
]
}
}
74 changes: 74 additions & 0 deletions fixtures/fizz/public/main.css
@@ -0,0 +1,74 @@
body {
font-family: system-ui, sans-serif;
}

* {
box-sizing: border-box;
}

nav {
padding: 20px;
}

.sidebar {
padding: 10px;
height: 500px;
float: left;
width: 30%;
}

.post {
padding: 20px;
float: left;
width: 60%;
}

h1, h2 {
padding: 0;
}

ul, li {
margin: 0;
}

.post p {
font-size: larger;
font-family: Georgia, serif;
}

.comments {
margin-top: 40px;
}

.comment {
border: 2px solid #aaa;
border-radius: 4px;
padding: 20px;
}

/* https://codepen.io/mandelid/pen/vwKoe */
.spinner {
display: inline-block;
transition: opacity linear 0.1s;
width: 20px;
height: 20px;
border: 3px solid rgba(80, 80, 80, 0.5);
border-radius: 50%;
border-top-color: #fff;
animation: spin 1s ease-in-out infinite;
opacity: 0;
}
.spinner--active {
opacity: 1;
}

@keyframes spin {
to {
transform: rotate(360deg);
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
53 changes: 53 additions & 0 deletions fixtures/fizz/scripts/build.js
@@ -0,0 +1,53 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

'use strict';

const path = require('path');
const rimraf = require('rimraf');
const webpack = require('webpack');

const isProduction = process.env.NODE_ENV === 'production';
rimraf.sync(path.resolve(__dirname, '../build'));
webpack(
{
mode: isProduction ? 'production' : 'development',
devtool: isProduction ? 'source-map' : 'cheap-module-source-map',
entry: [path.resolve(__dirname, '../src/index.js')],
output: {
path: path.resolve(__dirname, '../build'),
filename: 'main.js',
},
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader',
exclude: /node_modules/,
},
],
},
},
(err, stats) => {
if (err) {
console.error(err.stack || err);
if (err.details) {
console.error(err.details);
}
process.exit(1);
}
const info = stats.toJson();
if (stats.hasErrors()) {
console.log('Finished running webpack with errors.');
info.errors.forEach(e => console.error(e));
process.exit(1);
} else {
console.log('Finished running webpack.');
}
}
);
18 changes: 18 additions & 0 deletions fixtures/fizz/server/delays.js
@@ -0,0 +1,18 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

// Tweak these to play with different kinds of latency.

// How long the data fetches on the server.
exports.API_DELAY = 2000;

// How long the server waits for data before giving up.
exports.ABORT_DELAY = 10000;

// How long serving the JS bundles is delayed.
exports.JS_BUNDLE_DELAY = 4000;
83 changes: 83 additions & 0 deletions fixtures/fizz/server/render-to-buffer.js
@@ -0,0 +1,83 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

import {Writable} from 'stream';
import * as React from 'react';
import {renderToPipeableStream} from 'react-dom/server';
import App from '../src/App';
import {ABORT_DELAY} from './delays';

// In a real setup, you'd read it from webpack build stats.
let assets = {
'main.js': '/main.js',
'main.css': '/main.css',
};

function HtmlWritable(options) {
Writable.call(this, options);
this.chunks = [];
this.html = '';
}

HtmlWritable.prototype = Object.create(Writable.prototype);
HtmlWritable.prototype.getHtml = function getHtml() {
return this.html;
};
HtmlWritable.prototype._write = function _write(chunk, encoding, callback) {
this.chunks.push(chunk);
callback();
};
HtmlWritable.prototype._final = function _final(callback) {
this.html = Buffer.concat(this.chunks).toString();
callback();
};

module.exports = function render(url, res) {
let writable = new HtmlWritable();
res.socket.on('error', error => {
console.error('Fatal', error);
});
let didError = false;
let didFinish = false;

writable.on('finish', () => {
// If something errored before we started streaming, we set the error code appropriately.
res.statusCode = didError ? 500 : 200;
res.setHeader('Content-type', 'text/html');
res.send(writable.getHtml());
});

const {pipe, abort} = renderToPipeableStream(<App assets={assets} />, {
bootstrapScripts: [assets['main.js']],
onAllReady() {
// Full completion.
// You can use this for SSG or crawlers.
didFinish = true;
},
onShellReady() {
// If something errored before we started streaming, we set the error code appropriately.
pipe(writable);
},
onShellError(x) {
// Something errored before we could complete the shell so we emit an alternative shell.
res.statusCode = 500;
res.send('<!doctype><p>Error</p>');
},
onError(x) {
didError = true;
console.error(x);
},
});
// Abandon and switch to client rendering if enough time passes.
// Try lowering this to see the client recover.
setTimeout(() => {
if (!didFinish) {
abort();
}
}, ABORT_DELAY);
};
57 changes: 57 additions & 0 deletions fixtures/fizz/server/render-to-stream.js
@@ -0,0 +1,57 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

import * as React from 'react';
import {renderToPipeableStream} from 'react-dom/server';
import App from '../src/App';
import {ABORT_DELAY} from './delays';

// In a real setup, you'd read it from webpack build stats.
let assets = {
'main.js': '/main.js',
'main.css': '/main.css',
};

module.exports = function render(url, res) {
// The new wiring is a bit more involved.
res.socket.on('error', error => {
console.error('Fatal', error);
});
let didError = false;
let didFinish = false;
const {pipe, abort} = renderToPipeableStream(<App assets={assets} />, {
bootstrapScripts: [assets['main.js']],
onAllReady() {
// Full completion.
// You can use this for SSG or crawlers.
didFinish = true;
},
onShellReady() {
// If something errored before we started streaming, we set the error code appropriately.
res.statusCode = didError ? 500 : 200;
res.setHeader('Content-type', 'text/html');
setImmediate(() => pipe(res));
},
onShellError(x) {
// Something errored before we could complete the shell so we emit an alternative shell.
res.statusCode = 500;
res.send('<!doctype><p>Error</p>');
},
onError(x) {
didError = true;
console.error(x);
},
});
// Abandon and switch to client rendering if enough time passes.
// Try lowering this to see the client recover.
setTimeout(() => {
if (!didFinish) {
abort();
}
}, ABORT_DELAY);
};