Skip to content

Commit

Permalink
Updates flight client and associated webpack plugin to preinitialize …
Browse files Browse the repository at this point in the history
…imports when resolving client references on the server (SSR). The result is that the SSR stream will end up streaming in async scripts for the chunks needed to hydrate the SSR'd content instead of waiting for the flight payload to start processing rows on the client to discover imports there.

On the client however we need to be able to load the required chunks for a given import. We can't just use webpack's chunk loading because we don't have the chunkIds and are only transmitting the filepath. We implement our own chunk loading implementation which mimics webpack's with some differences. Namely there is no explicit timeout, we wait until the network fails if an earlier load or error even does not happen first.

One consequence of this approach is we may insert the same script twice for a chunk, once during SSR, and again when the flight client starts processing the flight payload for hydration. Since chunks register modules the operation is idempotent and as long as there is some cache-control in place for the resource the network requests should not be duplicated. This does mean however that it is important that if a chunk contains the webpack runtime it is not ever loaded using this custom loader implementation.
  • Loading branch information
gnoff committed May 8, 2023
1 parent 783e7fc commit 54f5559
Show file tree
Hide file tree
Showing 28 changed files with 348 additions and 113 deletions.
4 changes: 4 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,10 @@ module.exports = {
globals: {
__webpack_chunk_load__: 'readonly',
__webpack_require__: 'readonly',
__webpack_public_path__: 'readonly',
__webpack_nonce__: 'readonly',
__WEBPACK_FLIGHT_CROSS_ORIGIN_CREDENTIALS__: 'readonly',
__WEBPACK_FLIGHT_CROSS_ORIGIN_ANONYMOUS__: 'readonly',
},
},
{
Expand Down
27 changes: 14 additions & 13 deletions fixtures/flight/config/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -241,19 +241,20 @@ module.exports = function (webpackEnv) {
: isEnvDevelopment &&
(info => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/')),
},
cache: {
type: 'filesystem',
version: createEnvironmentHash(env.raw),
cacheDirectory: paths.appWebpackCache,
store: 'pack',
buildDependencies: {
defaultWebpack: ['webpack/lib/'],
config: [__filename],
tsconfig: [paths.appTsConfig, paths.appJsConfig].filter(f =>
fs.existsSync(f)
),
},
},
cache: false,
// cache: {
// type: 'filesystem',
// version: createEnvironmentHash(env.raw),
// cacheDirectory: paths.appWebpackCache,
// store: 'pack',
// buildDependencies: {
// defaultWebpack: ['webpack/lib/'],
// config: [__filename],
// tsconfig: [paths.appTsConfig, paths.appJsConfig].filter(f =>
// fs.existsSync(f)
// ),
// },
// },
infrastructureLogging: {
level: 'none',
},
Expand Down
39 changes: 33 additions & 6 deletions fixtures/flight/server/global.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const compress = require('compression');
const chalk = require('chalk');
const express = require('express');
const http = require('http');
const React = require('react');

const {renderToPipeableStream} = require('react-dom/server');
const {createFromNodeStream} = require('react-server-dom-webpack/client');
Expand Down Expand Up @@ -66,6 +67,11 @@ if (process.env.NODE_ENV === 'development') {
webpackMiddleware(compiler, {
publicPath: paths.publicUrlOrPath.slice(0, -1),
serverSideRender: true,
headers: () => {
return {
'Cache-Control': 'max-age=10, must-revalidate',
};
},
})
);
app.use(webpackHotMiddleware(compiler));
Expand Down Expand Up @@ -125,9 +131,9 @@ app.all('/', async function (req, res, next) {
buildPath = path.join(__dirname, '../build/');
}
// Read the module map from the virtual file system.
const moduleMap = JSON.parse(
const ssrBundleConfig = JSON.parse(
await virtualFs.readFile(
path.join(buildPath, 'react-ssr-manifest.json'),
path.join(buildPath, 'react-ssr-bundle-config.json'),
'utf8'
)
);
Expand All @@ -142,10 +148,17 @@ app.all('/', async function (req, res, next) {
// For HTML, we're a "client" emulator that runs the client code,
// so we start by consuming the RSC payload. This needs a module
// map that reverse engineers the client-side path to the SSR path.
const root = await createFromNodeStream(rscResponse, moduleMap);
let root;
let Root = () => {
if (root) {
return root;
}
root = createFromNodeStream(rscResponse, ssrBundleConfig);
return root;
};
// Render it into HTML by resolving the client components
res.set('Content-type', 'text/html');
const {pipe} = renderToPipeableStream(root, {
const {pipe} = renderToPipeableStream(React.createElement(Root), {
bootstrapScripts: mainJSChunks,
});
pipe(res);
Expand Down Expand Up @@ -175,10 +188,24 @@ app.all('/', async function (req, res, next) {
});

if (process.env.NODE_ENV === 'development') {
app.use(express.static('public'));
// app.use(
// express.static('public', {
// maxAge: 60000,
// setHeaders(res, path, stat) {
// res.set('x-timestamp', Date.now());
// },
// })
// );
} else {
// In production we host the static build output.
app.use(express.static('build'));
app.use(
express.static('build', {
maxAge: 60000,
setHeaders(res, path, stat) {
res.set('x-timestamp', Date.now());
},
})
);
}

app.listen(3000, () => {
Expand Down
6 changes: 3 additions & 3 deletions packages/react-client/src/ReactFlightClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import type {
ClientReferenceMetadata,
UninitializedModel,
Response,
SSRManifest,
BundleConfig,
} from './ReactFlightClientConfig';

import type {HintModel} from 'react-server/src/ReactFlightServerConfig';
Expand Down Expand Up @@ -159,7 +159,7 @@ Chunk.prototype.then = function <T>(
};

export type ResponseBase = {
_bundlerConfig: SSRManifest,
_bundlerConfig: BundleConfig,
_callServer: CallServerCallback,
_chunks: Map<number, SomeChunk<any>>,
...
Expand Down Expand Up @@ -654,7 +654,7 @@ function missingCall() {
}

export function createResponse(
bundlerConfig: SSRManifest,
bundlerConfig: BundleConfig,
callServer: void | CallServerCallback,
): ResponseBase {
const chunks: Map<number, SomeChunk<any>> = new Map();
Expand Down
4 changes: 2 additions & 2 deletions packages/react-client/src/ReactFlightClientStream.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import type {CallServerCallback} from './ReactFlightClient';
import type {Response} from './ReactFlightClientConfigStream';
import type {SSRManifest} from './ReactFlightClientConfig';
import type {BundleConfig} from './ReactFlightClientConfig';

import {
resolveModule,
Expand Down Expand Up @@ -127,7 +127,7 @@ function createFromJSONCallback(response: Response) {
}

export function createResponse(
bundlerConfig: SSRManifest,
bundlerConfig: BundleConfig,
callServer: void | CallServerCallback,
): Response {
// NOTE: CHECK THE COMPILER OUTPUT EACH TIME YOU CHANGE THIS.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@
export * from 'react-client/src/ReactFlightClientConfigBrowser';
export * from 'react-client/src/ReactFlightClientConfigStream';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigWebpackBundler';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigWebpackBrowser';
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@
export * from 'react-client/src/ReactFlightClientConfigBrowser';
export * from 'react-client/src/ReactFlightClientConfigStream';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigWebpackBundler';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigWebpackEdge';
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@
export * from 'react-client/src/ReactFlightClientConfigBrowser';
export * from 'react-client/src/ReactFlightClientConfigStream';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigWebpackBundler';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigWebpackBrowser';
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@
export * from 'react-client/src/ReactFlightClientConfigNode';
export * from 'react-client/src/ReactFlightClientConfigStream';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigWebpackBundler';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigWebpackNode';
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@
export * from 'react-client/src/ReactFlightClientConfigNode';
export * from 'react-client/src/ReactFlightClientConfigStream';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigNodeBundler';
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigWebpackNode';
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
*/

export * from 'react-server-dom-relay/src/ReactFlightClientConfigDOMRelay';
export * from '../ReactFlightClientConfigNoStream';
export * from 'react-client/src/ReactFlightClientConfigNoStream';
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@
*/

export * from 'react-server-native-relay/src/ReactFlightClientConfigNativeRelay';
export * from '../ReactFlightClientConfigNoStream';
export * from 'react-client/src/ReactFlightClientConfigNoStream';
21 changes: 7 additions & 14 deletions packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ import type {
import type {ReactScopeInstance} from 'shared/ReactTypes';
import type {AncestorInfoDev} from './validateDOMNesting';
import type {FormStatus} from 'react-dom-bindings/src/shared/ReactDOMFormActions';
import type {
PrefetchDNSOptions,
PreconnectOptions,
PreloadOptions,
PreinitOptions,
} from 'react-dom/src/ReactDOMDispatcher';

import {NotPending} from 'react-dom-bindings/src/shared/ReactDOMFormActions';
import {getCurrentRootHostContainer} from 'react-reconciler/src/ReactFiberHostContext';
Expand Down Expand Up @@ -2112,7 +2118,7 @@ function prefetchDNS(href: string, options?: mixed) {
preconnectAs('dns-prefetch', null, href);
}

function preconnect(href: string, options: ?{crossOrigin?: string}) {
function preconnect(href: string, options?: ?PreconnectOptions) {
if (!enableFloat) {
return;
}
Expand Down Expand Up @@ -2143,12 +2149,6 @@ function preconnect(href: string, options: ?{crossOrigin?: string}) {
preconnectAs('preconnect', crossOrigin, href);
}

type PreloadOptions = {
as: string,
crossOrigin?: string,
integrity?: string,
type?: string,
};
function preload(href: string, options: PreloadOptions) {
if (!enableFloat) {
return;
Expand Down Expand Up @@ -2223,13 +2223,6 @@ function preloadPropsFromPreloadOptions(
};
}

type PreinitOptions = {
as: string,
precedence?: string,
crossOrigin?: string,
integrity?: string,
nonce?: string,
};
function preinit(href: string, options: PreinitOptions) {
if (!enableFloat) {
return;
Expand Down
37 changes: 14 additions & 23 deletions packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@
*/

import type {ReactNodeList, ReactCustomFormAction} from 'shared/ReactTypes';
import type {FormStatus} from '../shared/ReactDOMFormActions';
import type {
Destination,
Chunk,
PrecomputedChunk,
} from 'react-server/src/ReactServerStreamConfig';
import type {
PrefetchDNSOptions,
PreconnectOptions,
PreloadOptions,
PreinitOptions,
} from 'react-dom/src/ReactDOMDispatcher';

import {
checkHtmlStringCoercion,
Expand All @@ -25,14 +37,6 @@ import {
enableFizzExternalRuntime,
} from 'shared/ReactFeatureFlags';

import type {
Destination,
Chunk,
PrecomputedChunk,
} from 'react-server/src/ReactServerStreamConfig';

import type {FormStatus} from '../shared/ReactDOMFormActions';

import {
writeChunk,
writeChunkAndReturn,
Expand Down Expand Up @@ -4966,7 +4970,7 @@ function getResourceKey(as: string, href: string): string {
return `[${as}]${href}`;
}

export function prefetchDNS(href: string, options?: mixed) {
export function prefetchDNS(href: string, options?: ?PrefetchDNSOptions) {
if (!enableFloat) {
return;
}
Expand Down Expand Up @@ -5025,7 +5029,7 @@ export function prefetchDNS(href: string, options?: mixed) {
}
}

export function preconnect(href: string, options?: ?{crossOrigin?: string}) {
export function preconnect(href: string, options?: ?PreconnectOptions) {
if (!enableFloat) {
return;
}
Expand Down Expand Up @@ -5088,12 +5092,6 @@ export function preconnect(href: string, options?: ?{crossOrigin?: string}) {
}
}

type PreloadOptions = {
as: string,
crossOrigin?: string,
integrity?: string,
type?: string,
};
export function preload(href: string, options: PreloadOptions) {
if (!enableFloat) {
return;
Expand Down Expand Up @@ -5232,13 +5230,6 @@ export function preload(href: string, options: PreloadOptions) {
}
}

type PreinitOptions = {
as: string,
precedence?: string,
crossOrigin?: string,
integrity?: string,
nonce?: string,
};
function preinit(href: string, options: PreinitOptions): void {
if (!enableFloat) {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,14 @@ export function dispatchHint(code: string, model: HintModel): void {
}
}
}

export function preinitModulesForSSR(href: string, crossOrigin: string | null) {
const dispatcher = ReactDOMCurrentDispatcher.current;
if (dispatcher) {
if (crossOrigin === null) {
dispatcher.preinit(href, {as: 'script'});
} else {
dispatcher.preinit(href, {as: 'script', crossOrigin});
}
}
}
2 changes: 1 addition & 1 deletion packages/react-dom/src/ReactDOMDispatcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export type PreinitOptions = {

export type HostDispatcher = {
prefetchDNS: (href: string, options?: ?PrefetchDNSOptions) => void,
preconnect: (href: string, options: ?PreconnectOptions) => void,
preconnect: (href: string, options?: ?PreconnectOptions) => void,
preload: (href: string, options: PreloadOptions) => void,
preinit: (href: string, options: PreinitOptions) => void,
};
Loading

0 comments on commit 54f5559

Please sign in to comment.