Skip to content

Commit

Permalink
[Float][Flight][Fizz][Fiber] Implement preloadModule and `preinitMo…
Browse files Browse the repository at this point in the history
…dule` (facebook#27220)

Stacked on facebook#27224 

### Implements `ReactDOM.preloadModule()`
`preloadModule` is a function to preload modules of various types.
Similar to `preload` this is useful when you expect to use a Resource
soon but can't render that resource directly. At the moment the only
sensible module to preload is script modules along with some other `as`
variants such as `as="serviceworker"`. In the future when there is some
notion of how to preload style module script or json module scripts this
API will be extended to support those as well.

##### Arguments
1. `href: string` -> the href or src value you want to preload.
2. `options?: {...}` -> 
2.1. `options.as?: string` -> the `as` property the modulepreload link
should render with. If not provided it will be omitted which will cause
the modulepreload to be treated like a script module
2.2. `options.crossOrigin?: string` -> modules always load with CORS but
you can provide `use-credentials` if you want to change the default
behavior
2.3. `options.integrity?: string` -> an integrity hash for subresource
integrity APIs
  
##### Rendering
each preloaded module will emit a `<link rel="modulepreload" href="..."
/>`
if `as` is specified and is something other than `"script"` the as
attribute will also be included
if crossOrigin or integrity as specified their attributes will also be
included

During SSR these script tags will be emitted before content. If we have
not yet flushed the document head they will be emitted there after
things that block paint such as font preloads, img preloads, and
stylesheets.

On the client these link tags will be appended to the document.head.
  
### Implements `ReactDOM.preinitModule()`
`preinitModule` is a function to loading module scripts before they are
required. It has the same use cases as `preinit`.

During SSR you would use this to tell the browsers to start fetching
code that will be used without having to wait for bootstrapping to
initiate module fetches.

ON the client you would use this to start fetching a module script early
for an anticipated navigation or other event that is likely to depend on
this module script.

the `as` property for Float methods drew inspiration from the `as`
attribute of the `<link rel="preload" ... >` tag but it is used as a
sort of tag for the kind of thing being targetted by Float methods. For
`preinitModule` we currently only support `as: "script"` and this is
also the assumed default type so you current never need to specify this
`as` value. In the future `preinitModule` will support additional module
script types such as `style` or `json`. The support of these types will
correspond to [Module Import
Attributes](https://github.com/tc39/proposal-import-attributes).

##### Arguments
1. `href: string` -> the href or src value you want to preinitialize
2. `options?: {...}` ->
2.1 `options.as?: string` -> only supports `script` and this is the
default behavior. Until we support import attributes such as `json` and
`style` there will not be much reason to provide an `as` option.
2.2. `options.crossOrigin?: string`: modules always load with CORS but
you can provide `use-credentials` if you want to change the default
behavior
2.3 `options.integrity?: string` -> an integrity hash for subresource
integrity APIs

##### Rendering
each preinitialized `script` module will emit a `<script type="module"
async="" src"...">` During SSR these will appear behind display blocking
resources such as font preloads, img preloads, and stylesheets. In the
browser these will be appende to the head.

Note that for other `as` types the rendered output will be slightly
different. `<script type="module">import "..." with {type: "json"
}</script>`. Since this tag is an inline script variants of React that
do not use inline scripts will simply omit these preinitialization tags
from the SSR output. This is not implemented in this PR but will appear
in a future update.
  • Loading branch information
gnoff authored and AndyPengc12 committed Apr 15, 2024
1 parent bdb218e commit afbb9cc
Show file tree
Hide file tree
Showing 15 changed files with 943 additions and 18 deletions.
224 changes: 221 additions & 3 deletions packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ import type {
PrefetchDNSOptions,
PreconnectOptions,
PreloadOptions,
PreloadModuleOptions,
PreinitOptions,
PreinitModuleOptions,
} from 'react-dom/src/shared/ReactDOMTypes';

import {NotPending} from 'react-dom-bindings/src/shared/ReactDOMFormActions';
Expand Down Expand Up @@ -2018,6 +2020,11 @@ type PreloadProps = {
href: ?string,
[string]: mixed,
};
type PreloadModuleProps = {
rel: 'modulepreload',
href: string,
[string]: mixed,
};

export type RootResources = {
hoistableStyles: Map<string, StyleResource>,
Expand All @@ -2029,7 +2036,8 @@ export function prepareToCommitHoistables() {
}

// global collections of Resources
const preloadPropsMap: Map<string, PreloadProps> = new Map();
const preloadPropsMap: Map<string, PreloadProps | PreloadModuleProps> =
new Map();
const preconnectsSet: Set<string> = new Set();

export type HoistableRoot = Document | ShadowRoot;
Expand Down Expand Up @@ -2060,7 +2068,9 @@ export const ReactDOMClientDispatcher: HostDispatcher = {
prefetchDNS,
preconnect,
preload,
preloadModule,
preinit,
preinitModule,
};

// We expect this to get inlined. It is a function mostly to communicate the special nature of
Expand Down Expand Up @@ -2264,6 +2274,86 @@ function preload(href: string, options: PreloadOptions) {
}
}
function preloadModule(href: string, options?: ?PreloadModuleOptions) {
if (!enableFloat) {
return;
}
if (__DEV__) {
let encountered = '';
if (typeof href !== 'string' || !href) {
encountered += ` The \`href\` argument encountered was ${getValueDescriptorExpectingObjectForWarning(
href,
)}.`;
}
if (options !== undefined && typeof options !== 'object') {
encountered += ` The \`options\` argument encountered was ${getValueDescriptorExpectingObjectForWarning(
options,
)}.`;
} else if (options && 'as' in options && typeof options.as !== 'string') {
encountered += ` The \`as\` option encountered was ${getValueDescriptorExpectingObjectForWarning(
options.as,
)}.`;
}
if (encountered) {
console.error(
'ReactDOM.preloadModule(): Expected two arguments, a non-empty `href` string and, optionally, an `options` object with an `as` property valid for a `<link rel="modulepreload" as="..." />` tag.%s',
encountered,
);
}
}
const ownerDocument = getDocumentForImperativeFloatMethods();
if (typeof href === 'string' && href) {
const as =
options && typeof options.as === 'string' ? options.as : 'script';
const preloadSelector = `link[rel="modulepreload"][as="${escapeSelectorAttributeValueInsideDoubleQuotes(
as,
)}"][href="${escapeSelectorAttributeValueInsideDoubleQuotes(href)}"]`;
// Some preloads are keyed under their selector. This happens when the preload is for
// an arbitrary type. Other preloads are keyed under the resource key they represent a preload for.
// Here we figure out which key to use to determine if we have a preload already.
let key = preloadSelector;
switch (as) {
case 'audioworklet':
case 'paintworklet':
case 'serviceworker':
case 'sharedworker':
case 'worker':
case 'script': {
key = getScriptKey(href);
break;
}
}

if (!preloadPropsMap.has(key)) {
const preloadProps = preloadModulePropsFromPreloadModuleOptions(
href,
as,
options,
);
preloadPropsMap.set(key, preloadProps);

if (null === ownerDocument.querySelector(preloadSelector)) {
switch (as) {
case 'audioworklet':
case 'paintworklet':
case 'serviceworker':
case 'sharedworker':
case 'worker':
case 'script': {
if (ownerDocument.querySelector(getScriptSelectorFromKey(key))) {
return;
}
}
}
const instance = ownerDocument.createElement('link');
setInitialProperties(instance, 'link', preloadProps);
markNodeAsHoistable(instance);
(ownerDocument.head: any).appendChild(instance);
}
}
}
}

function preloadPropsFromPreloadOptions(
href: string,
as: string,
Expand All @@ -2288,6 +2378,20 @@ function preloadPropsFromPreloadOptions(
};
}

function preloadModulePropsFromPreloadModuleOptions(
href: string,
as: string,
options: ?PreloadModuleOptions,
): PreloadModuleProps {
return {
rel: 'modulepreload',
as: as !== 'script' ? as : undefined,
href,
crossOrigin: options ? options.crossOrigin : undefined,
integrity: options ? options.integrity : undefined,
};
}

function preinit(href: string, options: PreinitOptions) {
if (!enableFloat) {
return;
Expand Down Expand Up @@ -2417,6 +2521,107 @@ function preinit(href: string, options: PreinitOptions) {
}
}

function preinitModule(href: string, options?: ?PreinitModuleOptions) {
if (!enableFloat) {
return;
}
if (__DEV__) {
let encountered = '';
if (typeof href !== 'string' || !href) {
encountered += ` The \`href\` argument encountered was ${getValueDescriptorExpectingObjectForWarning(
href,
)}.`;
}
if (options !== undefined && typeof options !== 'object') {
encountered += ` The \`options\` argument encountered was ${getValueDescriptorExpectingObjectForWarning(
options,
)}.`;
} else if (options && 'as' in options && options.as !== 'script') {
encountered += ` The \`as\` option encountered was ${getValueDescriptorExpectingEnumForWarning(
options.as,
)}.`;
}
if (encountered) {
console.error(
'ReactDOM.preinitModule(): Expected up to two arguments, a non-empty `href` string and, optionally, an `options` object with a valid `as` property.%s',
encountered,
);
} else {
const as =
options && typeof options.as === 'string' ? options.as : 'script';
switch (as) {
case 'script': {
break;
}

// We have an invalid as type and need to warn
default: {
const typeOfAs = getValueDescriptorExpectingEnumForWarning(as);
console.error(
'ReactDOM.preinitModule(): Currently the only supported "as" type for this function is "script"' +
' but received "%s" instead. This warning was generated for `href` "%s". In the future other' +
' module types will be supported, aligning with the import-attributes proposal. Learn more here:' +
' (https://github.com/tc39/proposal-import-attributes)',
typeOfAs,
href,
);
}
}
}
}
const ownerDocument = getDocumentForImperativeFloatMethods();

if (typeof href === 'string' && href) {
const as =
options && typeof options.as === 'string' ? options.as : 'script';

switch (as) {
case 'script': {
const src = href;
const scripts = getResourcesFromRoot(ownerDocument).hoistableScripts;

const key = getScriptKey(src);

// Check if this resource already exists
let resource = scripts.get(key);
if (resource) {
// We can early return. The resource exists and there is nothing
// more to do
return;
}

// Attempt to hydrate instance from DOM
let instance: null | Instance = ownerDocument.querySelector(
getScriptSelectorFromKey(key),
);
if (!instance) {
// Construct a new instance and insert it
const scriptProps = modulePropsFromPreinitModuleOptions(src, options);
// Adopt certain preload props
const preloadProps = preloadPropsMap.get(key);
if (preloadProps) {
adoptPreloadPropsForScript(scriptProps, preloadProps);
}
instance = ownerDocument.createElement('script');
markNodeAsHoistable(instance);
setInitialProperties(instance, 'link', scriptProps);
(ownerDocument.head: any).appendChild(instance);
}

// Construct a Resource and cache it
resource = {
type: 'script',
instance,
count: 1,
state: null,
};
scripts.set(key, resource);
return;
}
}
}
}

function stylesheetPropsFromPreinitOptions(
href: string,
precedence: string,
Expand Down Expand Up @@ -2446,6 +2651,19 @@ function scriptPropsFromPreinitOptions(
};
}

function modulePropsFromPreinitModuleOptions(
src: string,
options: ?PreinitModuleOptions,
): ScriptProps {
return {
src,
async: true,
type: 'module',
crossOrigin: options ? options.crossOrigin : undefined,
integrity: options ? options.integrity : undefined,
};
}

type StyleTagQualifyingProps = {
href: string,
precedence: string,
Expand Down Expand Up @@ -2854,7 +3072,7 @@ function insertStylesheet(
function adoptPreloadPropsForStylesheet(
stylesheetProps: StylesheetProps,
preloadProps: PreloadProps,
preloadProps: PreloadProps | PreloadModuleProps,
): void {
if (stylesheetProps.crossOrigin == null)
stylesheetProps.crossOrigin = preloadProps.crossOrigin;
Expand All @@ -2865,7 +3083,7 @@ function adoptPreloadPropsForStylesheet(
function adoptPreloadPropsForScript(
scriptProps: ScriptProps,
preloadProps: PreloadProps,
preloadProps: PreloadProps | PreloadModuleProps,
): void {
if (scriptProps.crossOrigin == null)
scriptProps.crossOrigin = preloadProps.crossOrigin;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import type {
PrefetchDNSOptions,
PreconnectOptions,
PreloadOptions,
PreloadModuleOptions,
PreinitOptions,
PreinitModuleOptions,
} from 'react-dom/src/shared/ReactDOMTypes';

import {enableFloat} from 'shared/ReactFeatureFlags';
Expand All @@ -27,7 +29,9 @@ export const ReactDOMFlightServerDispatcher: HostDispatcher = {
prefetchDNS,
preconnect,
preload,
preloadModule,
preinit,
preinitModule,
};

function prefetchDNS(href: string, options?: ?PrefetchDNSOptions) {
Expand Down Expand Up @@ -99,6 +103,28 @@ function preload(href: string, options: PreloadOptions) {
}
}

function preloadModule(href: string, options?: ?PreloadModuleOptions) {
if (enableFloat) {
if (typeof href === 'string') {
const request = resolveRequest();
if (request) {
const hints = getHints(request);
const key = 'm' + href;
if (hints.has(key)) {
// duplicate hint
return;
}
hints.add(key);
if (options) {
emitHint(request, 'm', [href, options]);
} else {
emitHint(request, 'm', href);
}
}
}
}
}

function preinit(href: string, options: PreinitOptions) {
if (enableFloat) {
if (typeof href === 'string') {
Expand All @@ -116,3 +142,25 @@ function preinit(href: string, options: PreinitOptions) {
}
}
}

function preinitModule(href: string, options?: ?PreinitModuleOptions) {
if (enableFloat) {
if (typeof href === 'string') {
const request = resolveRequest();
if (request) {
const hints = getHints(request);
const key = 'M' + href;
if (hints.has(key)) {
// duplicate hint
return;
}
hints.add(key);
if (options) {
emitHint(request, 'M', [href, options]);
} else {
emitHint(request, 'M', href);
}
}
}
}
}
Loading

0 comments on commit afbb9cc

Please sign in to comment.