[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.
This commit is contained in:
parent
0568c0f8cd
commit
fa58002262
|
@ -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).
|
|
@ -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/*"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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.');
|
||||
}
|
||||
}
|
||||
);
|
|
@ -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;
|
|
@ -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);
|
||||
};
|
|
@ -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);
|
||||
};
|
|
@ -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 =
|
||||
'<!DOCTYPE html>' +
|
||||
renderToString(<App assets={assets} />) +
|
||||
'<script src="/main.js" async=""></script>';
|
||||
let arr = textEncoder.encode(payload);
|
||||
|
||||
let buf = Buffer.from(arr);
|
||||
res.statusCode = 200;
|
||||
res.setHeader('Content-type', 'text/html');
|
||||
res.send(buf);
|
||||
};
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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(<BigComponent key={i} />);
|
||||
}
|
||||
|
||||
return (
|
||||
<Html assets={assets} title={title}>
|
||||
<h1>{title}</h1>
|
||||
{components}
|
||||
<h1>all done</h1>
|
||||
</Html>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,181 @@
|
|||
export default function BigComponent() {
|
||||
return (
|
||||
<article>
|
||||
<section>
|
||||
<h2>Description</h2>
|
||||
<p>
|
||||
This page has repeating sections purposefully to create very large
|
||||
trees that stress the rendering and streaming capabilities of Fizz
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Another Section</h2>
|
||||
<p>this section has a list</p>
|
||||
<ul>
|
||||
<li>item one</li>
|
||||
<li>item two</li>
|
||||
<li>item three</li>
|
||||
<li>item four</li>
|
||||
<li>item five</li>
|
||||
</ul>
|
||||
<p>it isn't a very interesting list</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Smiley Section</h2>
|
||||
<p>here is a list of smiley emojis</p>
|
||||
<ol>
|
||||
<li>😀</li>
|
||||
<li>😃</li>
|
||||
<li>😄</li>
|
||||
<li>😁</li>
|
||||
<li>😆</li>
|
||||
<li>😅</li>
|
||||
<li>😂</li>
|
||||
<li>🤣</li>
|
||||
<li>🥲</li>
|
||||
<li>☺️</li>
|
||||
<li>😊</li>
|
||||
<li>😇</li>
|
||||
<li>🙂</li>
|
||||
<li>🙃</li>
|
||||
<li>😉</li>
|
||||
<li>😌</li>
|
||||
<li>😍</li>
|
||||
<li>🥰</li>
|
||||
<li>😘</li>
|
||||
<li>😗</li>
|
||||
<li>😙</li>
|
||||
<li>😚</li>
|
||||
<li>😋</li>
|
||||
<li>😛</li>
|
||||
<li>😝</li>
|
||||
<li>😜</li>
|
||||
<li>🤪</li>
|
||||
<li>🤨</li>
|
||||
<li>🧐</li>
|
||||
<li>🤓</li>
|
||||
<li>😎</li>
|
||||
<li>🥸</li>
|
||||
<li>🤩</li>
|
||||
<li>🥳</li>
|
||||
<li>😏</li>
|
||||
<li>😒</li>
|
||||
<li>😞</li>
|
||||
<li>😔</li>
|
||||
<li>😟</li>
|
||||
<li>😕</li>
|
||||
<li>🙁</li>
|
||||
<li>☹️</li>
|
||||
<li>😣</li>
|
||||
<li>😖</li>
|
||||
<li>😫</li>
|
||||
<li>😩</li>
|
||||
<li>🥺</li>
|
||||
<li>😢</li>
|
||||
<li>😭</li>
|
||||
<li>😤</li>
|
||||
<li>😠</li>
|
||||
<li>😡</li>
|
||||
<li>🤬</li>
|
||||
<li>🤯</li>
|
||||
<li>😳</li>
|
||||
<li>🥵</li>
|
||||
<li>🥶</li>
|
||||
<li>😱</li>
|
||||
<li>😨</li>
|
||||
<li>😰</li>
|
||||
<li>😥</li>
|
||||
<li>😓</li>
|
||||
<li>🤗</li>
|
||||
<li>🤔</li>
|
||||
<li>🤭</li>
|
||||
<li>🤫</li>
|
||||
<li>🤥</li>
|
||||
<li>😶</li>
|
||||
<li>😐</li>
|
||||
<li>😑</li>
|
||||
<li>😬</li>
|
||||
<li>🙄</li>
|
||||
<li>😯</li>
|
||||
<li>😦</li>
|
||||
<li>😧</li>
|
||||
<li>😮</li>
|
||||
<li>😲</li>
|
||||
<li>🥱</li>
|
||||
<li>😴</li>
|
||||
<li>🤤</li>
|
||||
<li>😪</li>
|
||||
<li>😵</li>
|
||||
<li>🤐</li>
|
||||
<li>🥴</li>
|
||||
<li>🤢</li>
|
||||
<li>🤮</li>
|
||||
<li>🤧</li>
|
||||
<li>😷</li>
|
||||
<li>🤒</li>
|
||||
<li>🤕</li>
|
||||
<li>🤑</li>
|
||||
<li>🤠</li>
|
||||
<li>😈</li>
|
||||
<li>👿</li>
|
||||
<li>👹</li>
|
||||
<li>👺</li>
|
||||
<li>🤡</li>
|
||||
<li>💩</li>
|
||||
<li>👻</li>
|
||||
<li>💀</li>
|
||||
<li>☠️</li>
|
||||
<li>👽</li>
|
||||
<li>👾</li>
|
||||
<li>🤖</li>
|
||||
<li>🎃</li>
|
||||
<li>😺</li>
|
||||
<li>😸</li>
|
||||
<li>😹</li>
|
||||
<li>😻</li>
|
||||
<li>😼</li>
|
||||
<li>😽</li>
|
||||
<li>🙀</li>
|
||||
<li>😿</li>
|
||||
<li>😾</li>
|
||||
</ol>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Translation Section</h2>
|
||||
<p>This is the final section you will see before the sections repeat</p>
|
||||
<p>
|
||||
English: This is a text block translated from English to another
|
||||
language in Google Translate.
|
||||
</p>
|
||||
<p>
|
||||
Korean: 이것은 Google 번역에서 영어에서 다른 언어로 번역된 텍스트
|
||||
블록입니다.
|
||||
</p>
|
||||
<p>
|
||||
Hindi: यह Google अनुवाद में अंग्रेज़ी से दूसरी भाषा में अनुवादित
|
||||
टेक्स्ट ब्लॉक है।
|
||||
</p>
|
||||
<p>
|
||||
Lithuanian: Tai teksto blokas, išverstas iš anglų kalbos į kitą
|
||||
„Google“ vertėjo kalbą.
|
||||
</p>
|
||||
<div>
|
||||
<div>
|
||||
<div>
|
||||
<div>
|
||||
<div>
|
||||
<div>
|
||||
<span>
|
||||
we're deep in some nested divs here, not that you can tell
|
||||
visually
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
);
|
||||
}
|
|
@ -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 (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="shortcut icon" href="favicon.ico" />
|
||||
<link rel="stylesheet" href={assets['main.css']} />
|
||||
<title>{title}</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `<b>Enable JavaScript to run this app.</b>`,
|
||||
}}
|
||||
/>
|
||||
{children}
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `assetManifest = ${JSON.stringify(assets)};`,
|
||||
}}
|
||||
/>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
/**
|
||||
* 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 {hydrateRoot} from 'react-dom/client';
|
||||
import App from './App';
|
||||
|
||||
hydrateRoot(document, <App assets={window.assetManifest} />);
|
File diff suppressed because it is too large
Load Diff
|
@ -8,6 +8,7 @@
|
|||
*/
|
||||
|
||||
import type {Writable} from 'stream';
|
||||
import {TextEncoder} from 'util';
|
||||
|
||||
type MightBeFlushable = {
|
||||
flush?: () => void,
|
||||
|
@ -33,46 +34,153 @@ export function flushBuffered(destination: Destination) {
|
|||
}
|
||||
}
|
||||
|
||||
const VIEW_SIZE = 2048;
|
||||
let currentView = null;
|
||||
let writtenBytes = 0;
|
||||
let destinationHasCapacity = true;
|
||||
|
||||
export function beginWriting(destination: Destination) {
|
||||
// Older Node streams like http.createServer don't have this.
|
||||
if (typeof destination.cork === 'function') {
|
||||
destination.cork();
|
||||
currentView = new Uint8Array(VIEW_SIZE);
|
||||
writtenBytes = 0;
|
||||
destinationHasCapacity = true;
|
||||
}
|
||||
|
||||
function writeStringChunk(destination: Destination, stringChunk: string) {
|
||||
if (stringChunk.length === 0) {
|
||||
return;
|
||||
}
|
||||
// maximum possible view needed to encode entire string
|
||||
if (stringChunk.length * 3 > VIEW_SIZE) {
|
||||
if (writtenBytes > 0) {
|
||||
writeToDestination(
|
||||
destination,
|
||||
((currentView: any): Uint8Array).subarray(0, writtenBytes),
|
||||
);
|
||||
currentView = new Uint8Array(VIEW_SIZE);
|
||||
writtenBytes = 0;
|
||||
}
|
||||
writeToDestination(destination, textEncoder.encode(stringChunk));
|
||||
return;
|
||||
}
|
||||
|
||||
let target: Uint8Array = (currentView: any);
|
||||
if (writtenBytes > 0) {
|
||||
target = ((currentView: any): Uint8Array).subarray(writtenBytes);
|
||||
}
|
||||
const {read, written} = textEncoder.encodeInto(stringChunk, target);
|
||||
writtenBytes += written;
|
||||
|
||||
if (read < stringChunk.length) {
|
||||
writeToDestination(destination, (currentView: any));
|
||||
currentView = new Uint8Array(VIEW_SIZE);
|
||||
writtenBytes = textEncoder.encodeInto(stringChunk.slice(read), currentView)
|
||||
.written;
|
||||
}
|
||||
|
||||
if (writtenBytes === VIEW_SIZE) {
|
||||
writeToDestination(destination, (currentView: any));
|
||||
currentView = new Uint8Array(VIEW_SIZE);
|
||||
writtenBytes = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function writeViewChunk(destination: Destination, chunk: PrecomputedChunk) {
|
||||
if (chunk.byteLength === 0) {
|
||||
return;
|
||||
}
|
||||
if (chunk.byteLength > VIEW_SIZE) {
|
||||
// this chunk may overflow a single view which implies it was not
|
||||
// one that is cached by the streaming renderer. We will enqueu
|
||||
// it directly and expect it is not re-used
|
||||
if (writtenBytes > 0) {
|
||||
writeToDestination(
|
||||
destination,
|
||||
((currentView: any): Uint8Array).subarray(0, writtenBytes),
|
||||
);
|
||||
currentView = new Uint8Array(VIEW_SIZE);
|
||||
writtenBytes = 0;
|
||||
}
|
||||
writeToDestination(destination, chunk);
|
||||
return;
|
||||
}
|
||||
|
||||
let bytesToWrite = chunk;
|
||||
const allowableBytes = ((currentView: any): Uint8Array).length - writtenBytes;
|
||||
if (allowableBytes < bytesToWrite.byteLength) {
|
||||
// this chunk would overflow the current view. We enqueue a full view
|
||||
// and start a new view with the remaining chunk
|
||||
if (allowableBytes === 0) {
|
||||
// the current view is already full, send it
|
||||
writeToDestination(destination, (currentView: any));
|
||||
} else {
|
||||
// fill up the current view and apply the remaining chunk bytes
|
||||
// to a new view.
|
||||
((currentView: any): Uint8Array).set(
|
||||
bytesToWrite.subarray(0, allowableBytes),
|
||||
writtenBytes,
|
||||
);
|
||||
writtenBytes += allowableBytes;
|
||||
writeToDestination(destination, (currentView: any));
|
||||
bytesToWrite = bytesToWrite.subarray(allowableBytes);
|
||||
}
|
||||
currentView = new Uint8Array(VIEW_SIZE);
|
||||
writtenBytes = 0;
|
||||
}
|
||||
((currentView: any): Uint8Array).set(bytesToWrite, writtenBytes);
|
||||
writtenBytes += bytesToWrite.byteLength;
|
||||
|
||||
if (writtenBytes === VIEW_SIZE) {
|
||||
writeToDestination(destination, (currentView: any));
|
||||
currentView = new Uint8Array(VIEW_SIZE);
|
||||
writtenBytes = 0;
|
||||
}
|
||||
}
|
||||
|
||||
export function writeChunk(
|
||||
destination: Destination,
|
||||
chunk: Chunk | PrecomputedChunk,
|
||||
chunk: PrecomputedChunk | Chunk,
|
||||
): void {
|
||||
const nodeBuffer = ((chunk: any): Buffer | string); // close enough
|
||||
destination.write(nodeBuffer);
|
||||
if (typeof chunk === 'string') {
|
||||
writeStringChunk(destination, chunk);
|
||||
} else {
|
||||
writeViewChunk(destination, ((chunk: any): PrecomputedChunk));
|
||||
}
|
||||
}
|
||||
|
||||
function writeToDestination(destination: Destination, view: Uint8Array) {
|
||||
const currentHasCapacity = destination.write(view);
|
||||
destinationHasCapacity = destinationHasCapacity && currentHasCapacity;
|
||||
}
|
||||
|
||||
export function writeChunkAndReturn(
|
||||
destination: Destination,
|
||||
chunk: Chunk | PrecomputedChunk,
|
||||
chunk: PrecomputedChunk | Chunk,
|
||||
): boolean {
|
||||
const nodeBuffer = ((chunk: any): Buffer | string); // close enough
|
||||
return destination.write(nodeBuffer);
|
||||
writeChunk(destination, chunk);
|
||||
return destinationHasCapacity;
|
||||
}
|
||||
|
||||
export function completeWriting(destination: Destination) {
|
||||
// Older Node streams like http.createServer don't have this.
|
||||
if (typeof destination.uncork === 'function') {
|
||||
destination.uncork();
|
||||
if (currentView && writtenBytes > 0) {
|
||||
destination.write(currentView.subarray(0, writtenBytes));
|
||||
}
|
||||
currentView = null;
|
||||
writtenBytes = 0;
|
||||
destinationHasCapacity = true;
|
||||
}
|
||||
|
||||
export function close(destination: Destination) {
|
||||
destination.end();
|
||||
}
|
||||
|
||||
const textEncoder = new TextEncoder();
|
||||
|
||||
export function stringToChunk(content: string): Chunk {
|
||||
return content;
|
||||
}
|
||||
|
||||
export function stringToPrecomputedChunk(content: string): PrecomputedChunk {
|
||||
return Buffer.from(content, 'utf8');
|
||||
return textEncoder.encode(content);
|
||||
}
|
||||
|
||||
export function closeWithError(destination: Destination, error: mixed): void {
|
||||
|
|
|
@ -125,6 +125,33 @@ declare module 'pg' {
|
|||
};
|
||||
}
|
||||
|
||||
declare module 'util' {
|
||||
declare function debuglog(section: string): (data: any, ...args: any) => void;
|
||||
declare function format(format: string, ...placeholders: any): string;
|
||||
declare function log(string: string): void;
|
||||
declare function inspect(object: any, options?: util$InspectOptions): string;
|
||||
declare function isArray(object: any): boolean;
|
||||
declare function isRegExp(object: any): boolean;
|
||||
declare function isDate(object: any): boolean;
|
||||
declare function isError(object: any): boolean;
|
||||
declare function inherits(
|
||||
constructor: Function,
|
||||
superConstructor: Function,
|
||||
): void;
|
||||
declare function deprecate(f: Function, string: string): Function;
|
||||
declare function promisify(f: Function): Function;
|
||||
declare function callbackify(f: Function): Function;
|
||||
declare class TextEncoder {
|
||||
constructor(encoding?: string): TextEncoder;
|
||||
encode(buffer: string): Uint8Array;
|
||||
encodeInto(
|
||||
buffer: string,
|
||||
dest: Uint8Array,
|
||||
): {read: number, written: number};
|
||||
encoding: string;
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'pg/lib/utils' {
|
||||
declare module.exports: {
|
||||
prepareValue(val: any): mixed,
|
||||
|
|
|
@ -318,7 +318,7 @@ const bundles = [
|
|||
global: 'ReactDOMServer',
|
||||
minifyWithProdErrorCodes: false,
|
||||
wrapWithModuleBoundaries: false,
|
||||
externals: ['react'],
|
||||
externals: ['react', 'util'],
|
||||
},
|
||||
{
|
||||
bundleTypes: __EXPERIMENTAL__ ? [FB_WWW_DEV, FB_WWW_PROD] : [],
|
||||
|
@ -347,7 +347,7 @@ const bundles = [
|
|||
global: 'ReactServerDOMWriter',
|
||||
minifyWithProdErrorCodes: false,
|
||||
wrapWithModuleBoundaries: false,
|
||||
externals: ['react'],
|
||||
externals: ['react', 'util'],
|
||||
},
|
||||
|
||||
/******* React Server DOM Webpack Reader *******/
|
||||
|
@ -437,6 +437,7 @@ const bundles = [
|
|||
'ReactFlightNativeRelayServerIntegration',
|
||||
'JSResourceReferenceImpl',
|
||||
'ReactNativeInternalFeatureFlags',
|
||||
'util',
|
||||
],
|
||||
},
|
||||
|
||||
|
|
Loading…
Reference in New Issue