Skip to content

Commit d53877d

Browse files
committed
Updates flight client and associated webpack plugin to preinitialize 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.
1 parent 80a7f09 commit d53877d

37 files changed

+483
-148
lines changed

.eslintrc.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -426,7 +426,7 @@ module.exports = {
426426
files: ['packages/react-server-dom-webpack/**/*.js'],
427427
globals: {
428428
__webpack_chunk_load__: 'readonly',
429-
__webpack_require__: 'readonly',
429+
__webpack_require__: true,
430430
},
431431
},
432432
{

fixtures/flight/.nvmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
v18

fixtures/flight/config/webpack.config.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,12 @@ module.exports = function (webpackEnv) {
252252
tsconfig: [paths.appTsConfig, paths.appJsConfig].filter(f =>
253253
fs.existsSync(f)
254254
),
255+
react: [
256+
'react/',
257+
'react-dom/',
258+
'react-server-dom-webpack/',
259+
'scheduler/',
260+
],
255261
},
256262
},
257263
infrastructureLogging: {

fixtures/flight/server/global.js

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ const compress = require('compression');
3737
const chalk = require('chalk');
3838
const express = require('express');
3939
const http = require('http');
40+
const React = require('react');
4041

4142
const {renderToPipeableStream} = require('react-dom/server');
4243
const {createFromNodeStream} = require('react-server-dom-webpack/client');
@@ -66,6 +67,11 @@ if (process.env.NODE_ENV === 'development') {
6667
webpackMiddleware(compiler, {
6768
publicPath: paths.publicUrlOrPath.slice(0, -1),
6869
serverSideRender: true,
70+
headers: () => {
71+
return {
72+
'Cache-Control': 'no-store, must-revalidate',
73+
};
74+
},
6975
})
7076
);
7177
app.use(webpackHotMiddleware(compiler));
@@ -125,9 +131,9 @@ app.all('/', async function (req, res, next) {
125131
buildPath = path.join(__dirname, '../build/');
126132
}
127133
// Read the module map from the virtual file system.
128-
const moduleMap = JSON.parse(
134+
const ssrBundleConfig = JSON.parse(
129135
await virtualFs.readFile(
130-
path.join(buildPath, 'react-ssr-manifest.json'),
136+
path.join(buildPath, 'react-ssr-bundle-config.json'),
131137
'utf8'
132138
)
133139
);
@@ -142,10 +148,21 @@ app.all('/', async function (req, res, next) {
142148
// For HTML, we're a "client" emulator that runs the client code,
143149
// so we start by consuming the RSC payload. This needs a module
144150
// map that reverse engineers the client-side path to the SSR path.
145-
const root = await createFromNodeStream(rscResponse, moduleMap);
151+
let root;
152+
let Root = () => {
153+
if (root) {
154+
return root;
155+
}
156+
root = createFromNodeStream(
157+
rscResponse,
158+
ssrBundleConfig.chunkLoading,
159+
ssrBundleConfig.ssrManifest
160+
);
161+
return root;
162+
};
146163
// Render it into HTML by resolving the client components
147164
res.set('Content-type', 'text/html');
148-
const {pipe} = renderToPipeableStream(root, {
165+
const {pipe} = renderToPipeableStream(React.createElement(Root), {
149166
bootstrapScripts: mainJSChunks,
150167
});
151168
pipe(res);
@@ -177,7 +194,6 @@ app.all('/', async function (req, res, next) {
177194
if (process.env.NODE_ENV === 'development') {
178195
app.use(express.static('public'));
179196
} else {
180-
// In production we host the static build output.
181197
app.use(express.static('build'));
182198
}
183199

packages/react-client/src/ReactFlightClient.js

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {LazyComponent} from 'react/src/ReactLazy';
1313
import type {
1414
ClientReference,
1515
ClientReferenceMetadata,
16+
ChunkLoading,
1617
SSRManifest,
1718
StringDecoder,
1819
} from './ReactFlightClientConfig';
@@ -29,6 +30,7 @@ import {
2930
readPartialStringChunk,
3031
readFinalStringChunk,
3132
createStringDecoder,
33+
prepareDestinationForModule,
3234
} from './ReactFlightClientConfig';
3335

3436
import {
@@ -171,6 +173,7 @@ Chunk.prototype.then = function <T>(
171173

172174
export type Response = {
173175
_bundlerConfig: SSRManifest,
176+
_chunkLoading: ChunkLoading,
174177
_callServer: CallServerCallback,
175178
_chunks: Map<number, SomeChunk<any>>,
176179
_fromJSON: (key: string, value: JSONValue) => any,
@@ -678,11 +681,13 @@ function missingCall() {
678681

679682
export function createResponse(
680683
bundlerConfig: SSRManifest,
684+
chunkLoading: ChunkLoading,
681685
callServer: void | CallServerCallback,
682686
): Response {
683687
const chunks: Map<number, SomeChunk<any>> = new Map();
684688
const response: Response = {
685689
_bundlerConfig: bundlerConfig,
690+
_chunkLoading: chunkLoading,
686691
_callServer: callServer !== undefined ? callServer : missingCall,
687692
_chunks: chunks,
688693
_stringDecoder: createStringDecoder(),
@@ -719,17 +724,21 @@ function resolveText(response: Response, id: number, text: string): void {
719724
chunks.set(id, createInitializedTextChunk(response, text));
720725
}
721726

727+
function parseModuleModel(
728+
response: Response,
729+
model: UninitializedModel,
730+
): ClientReferenceMetadata {
731+
return parseModel(response, model);
732+
}
733+
722734
function resolveModule(
723735
response: Response,
724736
id: number,
725-
model: UninitializedModel,
737+
clientReferenceMetadata: ClientReferenceMetadata,
726738
): void {
727739
const chunks = response._chunks;
728740
const chunk = chunks.get(id);
729-
const clientReferenceMetadata: ClientReferenceMetadata = parseModel(
730-
response,
731-
model,
732-
);
741+
733742
const clientReference = resolveClientReference<$FlowFixMe>(
734743
response._bundlerConfig,
735744
clientReferenceMetadata,
@@ -857,7 +866,12 @@ function processFullRow(
857866
}
858867
switch (tag) {
859868
case 73 /* "I" */: {
860-
resolveModule(response, id, row);
869+
const clientReferenceMetadata = parseModuleModel(response, row);
870+
prepareDestinationForModule(
871+
response._chunkLoading,
872+
clientReferenceMetadata,
873+
);
874+
resolveModule(response, id, clientReferenceMetadata);
861875
return;
862876
}
863877
case 72 /* "H" */: {

packages/react-client/src/ReactFlightReplyClient.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,7 @@ export function processReply(
291291
// Possibly a Date, whose toJSON automatically calls toISOString
292292
// $FlowFixMe[incompatible-use]
293293
const originalValue = parent[key];
294+
// $FlowFixMe[method-unbinding]
294295
if (originalValue instanceof Date) {
295296
return serializeDateFromDateJSON(value);
296297
}

packages/react-client/src/forks/ReactFlightClientConfig.custom.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
declare var $$$config: any;
2727

28+
export opaque type ChunkLoading = mixed;
2829
export opaque type SSRManifest = mixed;
2930
export opaque type ServerManifest = mixed;
3031
export opaque type ServerReferenceId = string;
@@ -35,6 +36,8 @@ export const resolveServerReference = $$$config.resolveServerReference;
3536
export const preloadModule = $$$config.preloadModule;
3637
export const requireModule = $$$config.requireModule;
3738
export const dispatchHint = $$$config.dispatchHint;
39+
export const prepareDestinationForModule =
40+
$$$config.prepareDestinationForModule;
3841

3942
export opaque type Source = mixed;
4043

packages/react-client/src/forks/ReactFlightClientConfig.dom-browser.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,7 @@
88
*/
99

1010
export * from 'react-client/src/ReactFlightClientConfigBrowser';
11-
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigWebpackBundler';
11+
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack';
12+
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpackBrowser';
13+
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigWebpackDestinationClient';
1214
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';

packages/react-client/src/forks/ReactFlightClientConfig.dom-bun.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export * from 'react-client/src/ReactFlightClientConfigBrowser';
1111
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
1212

1313
export type Response = any;
14+
export opaque type ChunkLoading = mixed;
1415
export opaque type SSRManifest = mixed;
1516
export opaque type ServerManifest = mixed;
1617
export opaque type ServerReferenceId = string;

packages/react-client/src/forks/ReactFlightClientConfig.dom-edge-webpack.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,7 @@
88
*/
99

1010
export * from 'react-client/src/ReactFlightClientConfigBrowser';
11-
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigWebpackBundler';
11+
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpack';
12+
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigBundlerWebpackEdge';
13+
export * from 'react-server-dom-webpack/src/ReactFlightClientConfigWebpackDestinationServer';
1214
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';

0 commit comments

Comments
 (0)