-
Notifications
You must be signed in to change notification settings - Fork 47.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Fizz] Pipeable Stream Perf (#24291)
* Add fixture for comparing baseline render perf for renderToString and renderToPipeableStream Modified from ssr2 and https://github.com/SuperOleg39/react-ssr-perf-test * Implement buffering in pipeable streams The previous implementation of pipeable streaming (Node) suffered some performance issues brought about by the high chunk counts and innefficiencies with how node streams handle this situation. In particular the use of cork/uncork was meant to alleviate this but these methods do not do anything unless the receiving Writable Stream implements _writev which many won't. This change adopts the view based buffering techniques previously implemented for the Browser execution context. The main difference is the use of backpressure provided by the writable stream which is not implementable in the other context. Another change to note is the use of standards constructs like TextEncoder and TypedArrays. * Implement encodeInto during flushCompletedQueues encodeInto allows us to write directly to the view buffer that will end up getting streamed instead of encoding into an intermediate buffer and then copying that data.
- Loading branch information
Showing
17 changed files
with
6,168 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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/*" | ||
] | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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.'); | ||
} | ||
} | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
}; |
Oops, something went wrong.