diff --git a/fixtures/fizz/README.md b/fixtures/fizz/README.md new file mode 100644 index 0000000000000..63acd3ddc545d --- /dev/null +++ b/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). diff --git a/fixtures/fizz/package.json b/fixtures/fizz/package.json new file mode 100644 index 0000000000000..42becc4b8bce4 --- /dev/null +++ b/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/*" + ] + } +} diff --git a/fixtures/fizz/public/main.css b/fixtures/fizz/public/main.css new file mode 100644 index 0000000000000..34b4ecb1ce7f4 --- /dev/null +++ b/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); + } +} \ No newline at end of file diff --git a/fixtures/fizz/scripts/build.js b/fixtures/fizz/scripts/build.js new file mode 100644 index 0000000000000..452f7c750f61f --- /dev/null +++ b/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.'); + } + } +); diff --git a/fixtures/fizz/server/delays.js b/fixtures/fizz/server/delays.js new file mode 100644 index 0000000000000..20a1e870b4e0c --- /dev/null +++ b/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; diff --git a/fixtures/fizz/server/render-to-buffer.js b/fixtures/fizz/server/render-to-buffer.js new file mode 100644 index 0000000000000..ba509b812b733 --- /dev/null +++ b/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(, { + 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('

Error

'); + }, + 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); +}; diff --git a/fixtures/fizz/server/render-to-stream.js b/fixtures/fizz/server/render-to-stream.js new file mode 100644 index 0000000000000..063d776a8dd79 --- /dev/null +++ b/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(, { + 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('

Error

'); + }, + 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); +}; diff --git a/fixtures/fizz/server/render-to-string.js b/fixtures/fizz/server/render-to-string.js new file mode 100644 index 0000000000000..04ffff06b798b --- /dev/null +++ b/fixtures/fizz/server/render-to-string.js @@ -0,0 +1,34 @@ +/** + * 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 {renderToString} from 'react-dom/server'; +import App from '../src/App'; +import {API_DELAY, ABORT_DELAY} from './delays'; +import {performance} from 'perf_hooks'; + +// In a real setup, you'd read it from webpack build stats. +let assets = { + 'main.js': '/main.js', + 'main.css': '/main.css', +}; + +let textEncoder = new TextEncoder(); + +module.exports = function render(url, res) { + let payload = + '' + + renderToString() + + ''; + let arr = textEncoder.encode(payload); + + let buf = Buffer.from(arr); + res.statusCode = 200; + res.setHeader('Content-type', 'text/html'); + res.send(buf); +}; diff --git a/fixtures/fizz/server/server.js b/fixtures/fizz/server/server.js new file mode 100644 index 0000000000000..38519e5743105 --- /dev/null +++ b/fixtures/fizz/server/server.js @@ -0,0 +1,108 @@ +/** + * 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 babelRegister = require('@babel/register'); +babelRegister({ + ignore: [/[\\\/](build|server\/server|node_modules)[\\\/]/], + presets: [['react-app', {runtime: 'automatic'}]], + plugins: ['@babel/transform-modules-commonjs'], +}); + +const express = require('express'); +const compress = require('compression'); +const {readFileSync} = require('fs'); +const path = require('path'); +const renderToString = require('./render-to-string'); +const renderToStream = require('./render-to-stream'); +const renderToBuffer = require('./render-to-buffer'); +const {JS_BUNDLE_DELAY} = require('./delays'); + +const PORT = process.env.PORT || 4000; +const app = express(); + +app.use(compress()); +app.get( + '/', + handleErrors(async function(req, res) { + await waitForWebpack(); + renderToStream(req.url, res); + }) +); +app.get( + '/string', + handleErrors(async function(req, res) { + await waitForWebpack(); + renderToString(req.url, res); + }) +); +app.get( + '/stream', + handleErrors(async function(req, res) { + await waitForWebpack(); + renderToStream(req.url, res); + }) +); +app.get( + '/buffer', + handleErrors(async function(req, res) { + await waitForWebpack(); + renderToBuffer(req.url, res); + }) +); +app.use(express.static('build')); +app.use(express.static('public')); + +app + .listen(PORT, () => { + console.log(`Listening at ${PORT}...`); + }) + .on('error', function(error) { + if (error.syscall !== 'listen') { + throw error; + } + const isPipe = portOrPipe => Number.isNaN(portOrPipe); + const bind = isPipe(PORT) ? 'Pipe ' + PORT : 'Port ' + PORT; + switch (error.code) { + case 'EACCES': + console.error(bind + ' requires elevated privileges'); + process.exit(1); + break; + case 'EADDRINUSE': + console.error(bind + ' is already in use'); + process.exit(1); + break; + default: + throw error; + } + }); + +function handleErrors(fn) { + return async function(req, res, next) { + try { + return await fn(req, res); + } catch (x) { + next(x); + } + }; +} + +async function waitForWebpack() { + while (true) { + try { + readFileSync(path.resolve(__dirname, '../build/main.js')); + return; + } catch (err) { + console.log( + 'Could not find webpack build output. Will retry in a second...' + ); + await new Promise(resolve => setTimeout(resolve, 1000)); + } + } +} diff --git a/fixtures/fizz/src/App.js b/fixtures/fizz/src/App.js new file mode 100644 index 0000000000000..8610a4603cedc --- /dev/null +++ b/fixtures/fizz/src/App.js @@ -0,0 +1,26 @@ +/** + * 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 Html from './Html'; +import BigComponent from './BigComponent'; + +export default function App({assets, title}) { + const components = []; + + for (let i = 0; i <= 250; i++) { + components.push(); + } + + return ( + +

{title}

+ {components} +

all done

+ + ); +} diff --git a/fixtures/fizz/src/BigComponent.js b/fixtures/fizz/src/BigComponent.js new file mode 100644 index 0000000000000..e199199106840 --- /dev/null +++ b/fixtures/fizz/src/BigComponent.js @@ -0,0 +1,181 @@ +export default function BigComponent() { + return ( +
+
+

Description

+

+ This page has repeating sections purposefully to create very large + trees that stress the rendering and streaming capabilities of Fizz +

+
+
+

Another Section

+

this section has a list

+
    +
  • item one
  • +
  • item two
  • +
  • item three
  • +
  • item four
  • +
  • item five
  • +
+

it isn't a very interesting list

+
+
+

Smiley Section

+

here is a list of smiley emojis

+
    +
  1. ๐Ÿ˜€
  2. +
  3. ๐Ÿ˜ƒ
  4. +
  5. ๐Ÿ˜„
  6. +
  7. ๐Ÿ˜
  8. +
  9. ๐Ÿ˜†
  10. +
  11. ๐Ÿ˜…
  12. +
  13. ๐Ÿ˜‚
  14. +
  15. ๐Ÿคฃ
  16. +
  17. ๐Ÿฅฒ
  18. +
  19. โ˜บ๏ธ
  20. +
  21. ๐Ÿ˜Š
  22. +
  23. ๐Ÿ˜‡
  24. +
  25. ๐Ÿ™‚
  26. +
  27. ๐Ÿ™ƒ
  28. +
  29. ๐Ÿ˜‰
  30. +
  31. ๐Ÿ˜Œ
  32. +
  33. ๐Ÿ˜
  34. +
  35. ๐Ÿฅฐ
  36. +
  37. ๐Ÿ˜˜
  38. +
  39. ๐Ÿ˜—
  40. +
  41. ๐Ÿ˜™
  42. +
  43. ๐Ÿ˜š
  44. +
  45. ๐Ÿ˜‹
  46. +
  47. ๐Ÿ˜›
  48. +
  49. ๐Ÿ˜
  50. +
  51. ๐Ÿ˜œ
  52. +
  53. ๐Ÿคช
  54. +
  55. ๐Ÿคจ
  56. +
  57. ๐Ÿง
  58. +
  59. ๐Ÿค“
  60. +
  61. ๐Ÿ˜Ž
  62. +
  63. ๐Ÿฅธ
  64. +
  65. ๐Ÿคฉ
  66. +
  67. ๐Ÿฅณ
  68. +
  69. ๐Ÿ˜
  70. +
  71. ๐Ÿ˜’
  72. +
  73. ๐Ÿ˜ž
  74. +
  75. ๐Ÿ˜”
  76. +
  77. ๐Ÿ˜Ÿ
  78. +
  79. ๐Ÿ˜•
  80. +
  81. ๐Ÿ™
  82. +
  83. โ˜น๏ธ
  84. +
  85. ๐Ÿ˜ฃ
  86. +
  87. ๐Ÿ˜–
  88. +
  89. ๐Ÿ˜ซ
  90. +
  91. ๐Ÿ˜ฉ
  92. +
  93. ๐Ÿฅบ
  94. +
  95. ๐Ÿ˜ข
  96. +
  97. ๐Ÿ˜ญ
  98. +
  99. ๐Ÿ˜ค
  100. +
  101. ๐Ÿ˜ 
  102. +
  103. ๐Ÿ˜ก
  104. +
  105. ๐Ÿคฌ
  106. +
  107. ๐Ÿคฏ
  108. +
  109. ๐Ÿ˜ณ
  110. +
  111. ๐Ÿฅต
  112. +
  113. ๐Ÿฅถ
  114. +
  115. ๐Ÿ˜ฑ
  116. +
  117. ๐Ÿ˜จ
  118. +
  119. ๐Ÿ˜ฐ
  120. +
  121. ๐Ÿ˜ฅ
  122. +
  123. ๐Ÿ˜“
  124. +
  125. ๐Ÿค—
  126. +
  127. ๐Ÿค”
  128. +
  129. ๐Ÿคญ
  130. +
  131. ๐Ÿคซ
  132. +
  133. ๐Ÿคฅ
  134. +
  135. ๐Ÿ˜ถ
  136. +
  137. ๐Ÿ˜
  138. +
  139. ๐Ÿ˜‘
  140. +
  141. ๐Ÿ˜ฌ
  142. +
  143. ๐Ÿ™„
  144. +
  145. ๐Ÿ˜ฏ
  146. +
  147. ๐Ÿ˜ฆ
  148. +
  149. ๐Ÿ˜ง
  150. +
  151. ๐Ÿ˜ฎ
  152. +
  153. ๐Ÿ˜ฒ
  154. +
  155. ๐Ÿฅฑ
  156. +
  157. ๐Ÿ˜ด
  158. +
  159. ๐Ÿคค
  160. +
  161. ๐Ÿ˜ช
  162. +
  163. ๐Ÿ˜ต
  164. +
  165. ๐Ÿค
  166. +
  167. ๐Ÿฅด
  168. +
  169. ๐Ÿคข
  170. +
  171. ๐Ÿคฎ
  172. +
  173. ๐Ÿคง
  174. +
  175. ๐Ÿ˜ท
  176. +
  177. ๐Ÿค’
  178. +
  179. ๐Ÿค•
  180. +
  181. ๐Ÿค‘
  182. +
  183. ๐Ÿค 
  184. +
  185. ๐Ÿ˜ˆ
  186. +
  187. ๐Ÿ‘ฟ
  188. +
  189. ๐Ÿ‘น
  190. +
  191. ๐Ÿ‘บ
  192. +
  193. ๐Ÿคก
  194. +
  195. ๐Ÿ’ฉ
  196. +
  197. ๐Ÿ‘ป
  198. +
  199. ๐Ÿ’€
  200. +
  201. โ˜ ๏ธ
  202. +
  203. ๐Ÿ‘ฝ
  204. +
  205. ๐Ÿ‘พ
  206. +
  207. ๐Ÿค–
  208. +
  209. ๐ŸŽƒ
  210. +
  211. ๐Ÿ˜บ
  212. +
  213. ๐Ÿ˜ธ
  214. +
  215. ๐Ÿ˜น
  216. +
  217. ๐Ÿ˜ป
  218. +
  219. ๐Ÿ˜ผ
  220. +
  221. ๐Ÿ˜ฝ
  222. +
  223. ๐Ÿ™€
  224. +
  225. ๐Ÿ˜ฟ
  226. +
  227. ๐Ÿ˜พ
  228. +
+
+
+

Translation Section

+

This is the final section you will see before the sections repeat

+

+ English: This is a text block translated from English to another + language in Google Translate. +

+

+ Korean: ์ด๊ฒƒ์€ Google ๋ฒˆ์—ญ์—์„œ ์˜์–ด์—์„œ ๋‹ค๋ฅธ ์–ธ์–ด๋กœ ๋ฒˆ์—ญ๋œ ํ…์ŠคํŠธ + ๋ธ”๋ก์ž…๋‹ˆ๋‹ค. +

+

+ Hindi: เคฏเคน Google เค…เคจเฅเคตเคพเคฆ เคฎเฅ‡เค‚ เค…เค‚เค—เฅเคฐเฅ‡เคœเคผเฅ€ เคธเฅ‡ เคฆเฅ‚เคธเคฐเฅ€ เคญเคพเคทเคพ เคฎเฅ‡เค‚ เค…เคจเฅเคตเคพเคฆเคฟเคค + เคŸเฅ‡เค•เฅเคธเฅเคŸ เคฌเฅเคฒเฅ‰เค• เคนเฅˆเฅค +

+

+ Lithuanian: Tai teksto blokas, iลกverstas iลก anglลณ kalbos ฤฏ kitฤ… + โ€žGoogleโ€œ vertฤ—jo kalbฤ…. +

+
+
+
+
+
+
+ + we're deep in some nested divs here, not that you can tell + visually + +
+
+
+
+
+
+
+
+ ); +} diff --git a/fixtures/fizz/src/Html.js b/fixtures/fizz/src/Html.js new file mode 100644 index 0000000000000..e834f84bac872 --- /dev/null +++ b/fixtures/fizz/src/Html.js @@ -0,0 +1,34 @@ +/** + * 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. + * + */ + +export default function Html({assets, children, title}) { + return ( + + + + + + + {title} + + +