Skip to content

Commit 701ac2e

Browse files
authored
[Flight][Float] Preinitialize module imports during SSR (#27314)
Currently when we SSR a Flight response we do not emit any resources for module imports. This means that when the client hydrates it won't have already loaded the necessary scripts to satisfy the Imports defined in the Flight payload which will lead to a delay in hydration completing. This change updates `react-server-dom-webpack` and `react-server-dom-esm` to emit async script tags in the head when we encounter a modules in the flight response. To support this we need some additional server configuration. We need to know the path prefix for chunk loading and whether the chunks will load with CORS or not (and if so with what configuration).
1 parent 49eba01 commit 701ac2e

File tree

48 files changed

+1021
-288
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+1021
-288
lines changed

fixtures/flight-esm/.nvmrc

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

fixtures/flight-esm/server/global.js

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const compress = require('compression');
1010
const chalk = require('chalk');
1111
const express = require('express');
1212
const http = require('http');
13+
const React = require('react');
1314

1415
const {renderToPipeableStream} = require('react-dom/server');
1516
const {createFromNodeStream} = require('react-server-dom-esm/client');
@@ -62,23 +63,39 @@ app.all('/', async function (req, res, next) {
6263
if (req.accepts('text/html')) {
6364
try {
6465
const rscResponse = await promiseForData;
65-
6666
const moduleBaseURL = '/src';
6767

6868
// For HTML, we're a "client" emulator that runs the client code,
6969
// so we start by consuming the RSC payload. This needs the local file path
7070
// to load the source files from as well as the URL path for preloads.
71-
const root = await createFromNodeStream(
72-
rscResponse,
73-
moduleBasePath,
74-
moduleBaseURL
75-
);
71+
72+
let root;
73+
let Root = () => {
74+
if (root) {
75+
return React.use(root);
76+
}
77+
78+
return React.use(
79+
(root = createFromNodeStream(
80+
rscResponse,
81+
moduleBasePath,
82+
moduleBaseURL
83+
))
84+
);
85+
};
7686
// Render it into HTML by resolving the client components
7787
res.set('Content-type', 'text/html');
78-
const {pipe} = renderToPipeableStream(root, {
79-
// TODO: bootstrapModules inserts a preload before the importmap which causes
80-
// the import map to be invalid. We need to fix that in Float somehow.
81-
// bootstrapModules: ['/src/index.js'],
88+
const {pipe} = renderToPipeableStream(React.createElement(Root), {
89+
importMap: {
90+
imports: {
91+
react: 'https://esm.sh/react@experimental?pin=v124&dev',
92+
'react-dom': 'https://esm.sh/react-dom@experimental?pin=v124&dev',
93+
'react-dom/': 'https://esm.sh/react-dom@experimental&pin=v124&dev/',
94+
'react-server-dom-esm/client':
95+
'/node_modules/react-server-dom-esm/esm/react-server-dom-esm-client.browser.development.js',
96+
},
97+
},
98+
bootstrapModules: ['/src/index.js'],
8299
});
83100
pipe(res);
84101
} catch (e) {
@@ -89,6 +106,7 @@ app.all('/', async function (req, res, next) {
89106
} else {
90107
try {
91108
const rscResponse = await promiseForData;
109+
92110
// For other request, we pass-through the RSC payload.
93111
res.set('Content-type', 'text/x-component');
94112
rscResponse.on('data', data => {

fixtures/flight-esm/src/App.js

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,6 @@ import {getServerState} from './ServerState.js';
99

1010
const h = React.createElement;
1111

12-
const importMap = {
13-
imports: {
14-
react: 'https://esm.sh/react@experimental?pin=v124&dev',
15-
'react-dom': 'https://esm.sh/react-dom@experimental?pin=v124&dev',
16-
'react-dom/': 'https://esm.sh/react-dom@experimental&pin=v124&dev/',
17-
'react-server-dom-esm/client':
18-
'/node_modules/react-server-dom-esm/esm/react-server-dom-esm-client.browser.development.js',
19-
},
20-
};
21-
2212
export default async function App() {
2313
const res = await fetch('http://localhost:3001/todos');
2414
const todos = await res.json();
@@ -42,12 +32,6 @@ export default async function App() {
4232
rel: 'stylesheet',
4333
href: '/src/style.css',
4434
precedence: 'default',
45-
}),
46-
h('script', {
47-
type: 'importmap',
48-
dangerouslySetInnerHTML: {
49-
__html: JSON.stringify(importMap),
50-
},
5135
})
5236
),
5337
h(
@@ -84,9 +68,7 @@ export default async function App() {
8468
'Like'
8569
)
8670
)
87-
),
88-
// TODO: Move this to bootstrapModules.
89-
h('script', {type: 'module', src: '/src/index.js'})
71+
)
9072
)
9173
);
9274
}

fixtures/flight-esm/yarn.lock

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -540,17 +540,17 @@ [email protected]:
540540
unpipe "1.0.0"
541541

542542
react-dom@experimental:
543-
version "0.0.0-experimental-018c58c9c-20230601"
544-
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-0.0.0-experimental-018c58c9c-20230601.tgz#2cc0ac824b83bab2ac1c6187f241dbd5dcd5201b"
545-
integrity sha512-hwRsyoG1R3Tub0nUa72YvNcqPvU+pTcr9dadOnUCKKfSiYVbBCy7LxmkqLauCD8OjNJMlwtMgG4UAgtidclYGQ==
543+
version "0.0.0-experimental-b9be4537c-20230905"
544+
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-0.0.0-experimental-b9be4537c-20230905.tgz#b078d6d06041e0c98ce5a2f5e9ff26a2e308eb41"
545+
integrity sha512-veAFNVj81lUYhYlucYm3kbj2BhakG57XYkWC/QHVEZDk4Hm2qxM9RUk7gn8dWs9Eq7KR6Q+JWiSH3ZbObQTV9g==
546546
dependencies:
547547
loose-envify "^1.1.0"
548-
scheduler "0.0.0-experimental-018c58c9c-20230601"
548+
scheduler "0.0.0-experimental-b9be4537c-20230905"
549549

550550
react@experimental:
551-
version "0.0.0-experimental-018c58c9c-20230601"
552-
resolved "https://registry.yarnpkg.com/react/-/react-0.0.0-experimental-018c58c9c-20230601.tgz#ab04d1243c8f83b0166ed342056fa6b38ab2cd23"
553-
integrity sha512-nSQIBsZ26Ii899pZ9cRt/6uQLbIUEAcDIivvAQyaHp4pWm289aB+7AK7VCWojAJIf4OStCuWs2berZsk4mzLVg==
551+
version "0.0.0-experimental-b9be4537c-20230905"
552+
resolved "https://registry.yarnpkg.com/react/-/react-0.0.0-experimental-b9be4537c-20230905.tgz#3c2352b42b8024544a12dcd96f2700313cebcb6b"
553+
integrity sha512-QNeK74S7AU94j4vCxet2S76HqxpF6CJo1pG3XcgY2NravyXdWYszrRDNHrfu86gGNwAQvSU+YpStYn/i0b9tLA==
554554
dependencies:
555555
loose-envify "^1.1.0"
556556

@@ -588,10 +588,10 @@ [email protected]:
588588
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
589589
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
590590

591-
[email protected]018c58c9c-20230601:
592-
version "0.0.0-experimental-018c58c9c-20230601"
593-
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.0.0-experimental-018c58c9c-20230601.tgz#4f083614f8e857bab63dd90b4b37b03783dafe6b"
594-
integrity sha512-otUM7AAAnCoJ5/0jTQwUQ7NhxjgcPEdrfzW7NfkpocrDoTUbql1kIGIhj9L9POMVFDI/wcZzRNK/oIEWsB4DPw==
591+
[email protected]b9be4537c-20230905:
592+
version "0.0.0-experimental-b9be4537c-20230905"
593+
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.0.0-experimental-b9be4537c-20230905.tgz#f0fe5a710ce15a9d637c28e9f019a4100e1f3f34"
594+
integrity sha512-V5P9LOS+c5CG7qaCJu+Qgcz9eh/dP4nBszj3w1MCgZnMtAna6+J8ZuuUnRDMeY86F8KH+cY8Q5beIvAL2noMzA==
595595
dependencies:
596596
loose-envify "^1.1.0"
597597

fixtures/flight/.nvmrc

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

fixtures/flight/server/global.js

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const compress = require('compression');
3333
const chalk = require('chalk');
3434
const express = require('express');
3535
const http = require('http');
36+
const React = require('react');
3637

3738
const {renderToPipeableStream} = require('react-dom/server');
3839
const {createFromNodeStream} = require('react-server-dom-webpack/client');
@@ -62,6 +63,11 @@ if (process.env.NODE_ENV === 'development') {
6263
webpackMiddleware(compiler, {
6364
publicPath: paths.publicUrlOrPath.slice(0, -1),
6465
serverSideRender: true,
66+
headers: () => {
67+
return {
68+
'Cache-Control': 'no-store, must-revalidate',
69+
};
70+
},
6571
})
6672
);
6773
app.use(webpackHotMiddleware(compiler));
@@ -121,12 +127,13 @@ app.all('/', async function (req, res, next) {
121127
buildPath = path.join(__dirname, '../build/');
122128
}
123129
// Read the module map from the virtual file system.
124-
const moduleMap = JSON.parse(
130+
const ssrManifest = JSON.parse(
125131
await virtualFs.readFile(
126132
path.join(buildPath, 'react-ssr-manifest.json'),
127133
'utf8'
128134
)
129135
);
136+
130137
// Read the entrypoints containing the initial JS to bootstrap everything.
131138
// For other pages, the chunks in the RSC payload are enough.
132139
const mainJSChunks = JSON.parse(
@@ -138,15 +145,35 @@ app.all('/', async function (req, res, next) {
138145
// For HTML, we're a "client" emulator that runs the client code,
139146
// so we start by consuming the RSC payload. This needs a module
140147
// map that reverse engineers the client-side path to the SSR path.
141-
const {root, formState} = await createFromNodeStream(
142-
rscResponse,
143-
moduleMap
144-
);
148+
149+
// This is a bad hack to set the form state after SSR has started. It works
150+
// because we block the root component until we have the form state and
151+
// any form that reads it necessarily will come later. It also only works
152+
// because the formstate type is an object which may change in the future
153+
const lazyFormState = [];
154+
155+
let cachedResult = null;
156+
async function getRootAndFormState() {
157+
const {root, formState} = await createFromNodeStream(
158+
rscResponse,
159+
ssrManifest
160+
);
161+
// We shouldn't be assuming formState is an object type but at the moment
162+
// we have no way of setting the form state from within the render
163+
Object.assign(lazyFormState, formState);
164+
return root;
165+
}
166+
let Root = () => {
167+
if (!cachedResult) {
168+
cachedResult = getRootAndFormState();
169+
}
170+
return React.use(cachedResult);
171+
};
145172
// Render it into HTML by resolving the client components
146173
res.set('Content-type', 'text/html');
147-
const {pipe} = renderToPipeableStream(root, {
174+
const {pipe} = renderToPipeableStream(React.createElement(Root), {
148175
bootstrapScripts: mainJSChunks,
149-
experimental_formState: formState,
176+
experimental_formState: lazyFormState,
150177
});
151178
pipe(res);
152179
} catch (e) {

packages/react-client/src/ReactFlightClient.js

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ import type {LazyComponent} from 'react/src/ReactLazy';
1313
import type {
1414
ClientReference,
1515
ClientReferenceMetadata,
16-
SSRManifest,
16+
SSRModuleMap,
1717
StringDecoder,
18+
ModuleLoading,
1819
} from './ReactFlightClientConfig';
1920

2021
import type {
@@ -36,6 +37,7 @@ import {
3637
readPartialStringChunk,
3738
readFinalStringChunk,
3839
createStringDecoder,
40+
prepareDestinationForModule,
3941
} from './ReactFlightClientConfig';
4042

4143
import {registerServerReference} from './ReactFlightReplyClient';
@@ -178,8 +180,10 @@ Chunk.prototype.then = function <T>(
178180
};
179181

180182
export type Response = {
181-
_bundlerConfig: SSRManifest,
183+
_bundlerConfig: SSRModuleMap,
184+
_moduleLoading: ModuleLoading,
182185
_callServer: CallServerCallback,
186+
_nonce: ?string,
183187
_chunks: Map<number, SomeChunk<any>>,
184188
_fromJSON: (key: string, value: JSONValue) => any,
185189
_stringDecoder: StringDecoder,
@@ -706,13 +710,17 @@ function missingCall() {
706710
}
707711

708712
export function createResponse(
709-
bundlerConfig: SSRManifest,
713+
bundlerConfig: SSRModuleMap,
714+
moduleLoading: ModuleLoading,
710715
callServer: void | CallServerCallback,
716+
nonce: void | string,
711717
): Response {
712718
const chunks: Map<number, SomeChunk<any>> = new Map();
713719
const response: Response = {
714720
_bundlerConfig: bundlerConfig,
721+
_moduleLoading: moduleLoading,
715722
_callServer: callServer !== undefined ? callServer : missingCall,
723+
_nonce: nonce,
716724
_chunks: chunks,
717725
_stringDecoder: createStringDecoder(),
718726
_fromJSON: (null: any),
@@ -774,6 +782,12 @@ function resolveModule(
774782
clientReferenceMetadata,
775783
);
776784

785+
prepareDestinationForModule(
786+
response._moduleLoading,
787+
response._nonce,
788+
clientReferenceMetadata,
789+
);
790+
777791
// TODO: Add an option to encode modules that are lazy loaded.
778792
// For now we preload all modules as early as possible since it's likely
779793
// that we'll need them.

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525

2626
declare var $$$config: any;
2727

28-
export opaque type SSRManifest = mixed;
28+
export opaque type ModuleLoading = mixed;
29+
export opaque type SSRModuleMap = mixed;
2930
export opaque type ServerManifest = mixed;
3031
export opaque type ServerReferenceId = string;
3132
export opaque type ClientReferenceMetadata = mixed;
@@ -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
export const usedWithSSR = true;
3942

4043
export opaque type Source = mixed;

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

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

1010
export * from 'react-client/src/ReactFlightClientConfigBrowser';
11-
export * from 'react-server-dom-esm/src/ReactFlightClientConfigESMBundler';
11+
export * from 'react-server-dom-esm/src/ReactFlightClientConfigBundlerESM';
12+
export * from 'react-server-dom-esm/src/ReactFlightClientConfigTargetESMBrowser';
1213
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
1314
export const usedWithSSR = false;

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
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/ReactFlightClientConfigTargetWebpackBrowser';
1214
export * from 'react-dom-bindings/src/shared/ReactFlightClientConfigDOM';
1315
export const usedWithSSR = false;

0 commit comments

Comments
 (0)