Skip to content

Commit e1ad4aa

Browse files
authored
[Fizz][Float] stop automatically preloading scripts that are not script resources (#26877)
Currently we preload all scripts that are not hoisted. One of the original reasons for this is we stopped SSR rendering async scripts that had an onLoad/onError because we needed to be able to distinguish between Float scripts and non-Float scripts during hydration. Hydration has been refactored a bit and we can not get around this limitation so we can just emit the async script in place. However, sync and defer scripts are also preloaded. While this is sometimes desirable it is not universally so and there are issues with conveying priority properly (see fetchpriority) so with this change we remove the automatic preloading of non-Float scripts altogether. For this change to make sense we also need to emit async scripts with loading handlers during SSR. we previously only preloaded them during SSR because it was necessary to keep async scripts as unambiguously resources when hydrating. One ancillary benefit was that load handlers would always fire b/c there was no chance the script would run before hydration. With this change we go back to having the ability to have load handlers fired before hydration. This is already a problem with images and we don't have a generalized solution for it however our likely approach to this sort of thing where you need to wait for a script to load is to use something akin to `importScripts()` rather than rendering a script with onLoad.
1 parent 5fb2c15 commit e1ad4aa

File tree

6 files changed

+102
-228
lines changed

6 files changed

+102
-228
lines changed

packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js

Lines changed: 10 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1036,19 +1036,6 @@ export function bindInstance(
10361036

10371037
export const supportsHydration = true;
10381038

1039-
// With Resources, some HostComponent types will never be server rendered and need to be
1040-
// inserted without breaking hydration
1041-
export function isHydratableType(type: string, props: Props): boolean {
1042-
if (enableFloat) {
1043-
if (type === 'script') {
1044-
const {async, onLoad, onError} = (props: any);
1045-
return !(async && (onLoad || onError));
1046-
}
1047-
return true;
1048-
} else {
1049-
return true;
1050-
}
1051-
}
10521039
export function isHydratableText(text: string): boolean {
10531040
return text !== '';
10541041
}
@@ -1164,21 +1151,22 @@ export function canHydrateInstance(
11641151
// if we learn it is problematic
11651152
const srcAttr = element.getAttribute('src');
11661153
if (
1167-
srcAttr &&
1168-
element.hasAttribute('async') &&
1169-
!element.hasAttribute('itemprop')
1170-
) {
1171-
// This is an async script resource
1172-
break;
1173-
} else if (
11741154
srcAttr !== (anyProps.src == null ? null : anyProps.src) ||
11751155
element.getAttribute('type') !==
11761156
(anyProps.type == null ? null : anyProps.type) ||
11771157
element.getAttribute('crossorigin') !==
11781158
(anyProps.crossOrigin == null ? null : anyProps.crossOrigin)
11791159
) {
1180-
// This script is for a different src
1181-
break;
1160+
// This script is for a different src/type/crossOrigin. It may be a script resource
1161+
// or it may just be a mistmatch
1162+
if (
1163+
srcAttr &&
1164+
element.hasAttribute('async') &&
1165+
!element.hasAttribute('itemprop')
1166+
) {
1167+
// This is an async script resource
1168+
break;
1169+
}
11821170
}
11831171
return element;
11841172
}

packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js

Lines changed: 78 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -2642,128 +2642,102 @@ function pushScript(
26422642
noscriptTagInScope: boolean,
26432643
): null {
26442644
if (enableFloat) {
2645+
const asyncProp = props.async;
26452646
if (
2647+
typeof props.src !== 'string' ||
2648+
!props.src ||
2649+
!(
2650+
asyncProp &&
2651+
typeof asyncProp !== 'function' &&
2652+
typeof asyncProp !== 'symbol'
2653+
) ||
2654+
props.onLoad ||
2655+
props.onError ||
26462656
insertionMode === SVG_MODE ||
26472657
noscriptTagInScope ||
2648-
props.itemProp != null ||
2649-
typeof props.src !== 'string' ||
2650-
!props.src
2658+
props.itemProp != null
26512659
) {
2652-
// This script will not be a resource nor can it be preloaded, we bailout early
2653-
// and emit it in place.
2660+
// This script will not be a resource, we bailout early and emit it in place.
26542661
return pushScriptImpl(target, props);
26552662
}
26562663

26572664
const src = props.src;
26582665
const key = getResourceKey('script', src);
2659-
if (props.async !== true || props.onLoad || props.onError) {
2660-
// we don't want to preload nomodule scripts
2661-
if (props.noModule !== true) {
2662-
// We can't resourcify scripts with load listeners. To avoid ambiguity with
2663-
// other Resourcified async scripts on the server we omit them from the server
2664-
// stream and expect them to be inserted during hydration on the client.
2665-
// We can still preload them however so the client can start fetching the script
2666-
// as soon as possible
2667-
let resource = resources.preloadsMap.get(key);
2668-
if (!resource) {
2669-
resource = {
2670-
type: 'preload',
2671-
chunks: [],
2672-
state: NoState,
2673-
props: preloadAsScriptPropsFromProps(props.src, props),
2674-
};
2675-
resources.preloadsMap.set(key, resource);
2676-
if (__DEV__) {
2677-
markAsImplicitResourceDEV(resource, props, resource.props);
2666+
// We can make this <script> into a ScriptResource
2667+
let resource = resources.scriptsMap.get(key);
2668+
if (__DEV__) {
2669+
const devResource = getAsResourceDEV(resource);
2670+
if (devResource) {
2671+
switch (devResource.__provenance) {
2672+
case 'rendered': {
2673+
const differenceDescription = describeDifferencesForScripts(
2674+
// Diff the props from the JSX element, not the derived resource props
2675+
props,
2676+
devResource.__originalProps,
2677+
);
2678+
if (differenceDescription) {
2679+
console.error(
2680+
'React encountered a <script async={true} src="%s" .../> that has props that conflict' +
2681+
' with another hoistable script with the same `src`. When rendering hoistable scripts (async scripts without any loading handlers)' +
2682+
' the props from the first encountered instance will be used and props from later instances will be ignored.' +
2683+
' Update the props on both <script async={true} .../> instance so they agree.%s',
2684+
src,
2685+
differenceDescription,
2686+
);
2687+
}
2688+
break;
26782689
}
2679-
resources.usedScripts.add(resource);
2680-
pushLinkImpl(resource.chunks, resource.props);
2681-
}
2682-
}
2683-
2684-
if (props.async !== true) {
2685-
// This is not an async script, we can preloaded it but it still needs to
2686-
// be emitted in place since it needs to hydrate on the client
2687-
pushScriptImpl(target, props);
2688-
return null;
2689-
}
2690-
} else {
2691-
// We can make this <script> into a ScriptResource
2692-
let resource = resources.scriptsMap.get(key);
2693-
if (__DEV__) {
2694-
const devResource = getAsResourceDEV(resource);
2695-
if (devResource) {
2696-
switch (devResource.__provenance) {
2697-
case 'rendered': {
2698-
const differenceDescription = describeDifferencesForScripts(
2690+
case 'preinit': {
2691+
const differenceDescription =
2692+
describeDifferencesForScriptOverPreinit(
26992693
// Diff the props from the JSX element, not the derived resource props
27002694
props,
2701-
devResource.__originalProps,
2695+
devResource.__propsEquivalent,
2696+
);
2697+
if (differenceDescription) {
2698+
console.error(
2699+
'React encountered a <script async={true} src="%s" .../> with props that conflict' +
2700+
' with the options provided to `ReactDOM.preinit("%s", { as: "script", ... })`. React will use the first props or preinitialization' +
2701+
' options encountered when rendering a hoistable script with a particular `src` and will ignore any newer props or' +
2702+
' options. The first instance of this script resource was created using the `ReactDOM.preinit()` function.' +
2703+
' Please note, `ReactDOM.preinit()` is modeled off of module import assertions capabilities and does not support' +
2704+
' arbitrary props. If you need to have props not included with the preinit options you will need to rely on rendering' +
2705+
' <script> tags only.%s',
2706+
src,
2707+
src,
2708+
differenceDescription,
27022709
);
2703-
if (differenceDescription) {
2704-
console.error(
2705-
'React encountered a <script async={true} src="%s" .../> that has props that conflict' +
2706-
' with another hoistable script with the same `src`. When rendering hoistable scripts (async scripts without any loading handlers)' +
2707-
' the props from the first encountered instance will be used and props from later instances will be ignored.' +
2708-
' Update the props on both <script async={true} .../> instance so they agree.%s',
2709-
src,
2710-
differenceDescription,
2711-
);
2712-
}
2713-
break;
2714-
}
2715-
case 'preinit': {
2716-
const differenceDescription =
2717-
describeDifferencesForScriptOverPreinit(
2718-
// Diff the props from the JSX element, not the derived resource props
2719-
props,
2720-
devResource.__propsEquivalent,
2721-
);
2722-
if (differenceDescription) {
2723-
console.error(
2724-
'React encountered a <script async={true} src="%s" .../> with props that conflict' +
2725-
' with the options provided to `ReactDOM.preinit("%s", { as: "script", ... })`. React will use the first props or preinitialization' +
2726-
' options encountered when rendering a hoistable script with a particular `src` and will ignore any newer props or' +
2727-
' options. The first instance of this script resource was created using the `ReactDOM.preinit()` function.' +
2728-
' Please note, `ReactDOM.preinit()` is modeled off of module import assertions capabilities and does not support' +
2729-
' arbitrary props. If you need to have props not included with the preinit options you will need to rely on rendering' +
2730-
' <script> tags only.%s',
2731-
src,
2732-
src,
2733-
differenceDescription,
2734-
);
2735-
}
2736-
break;
27372710
}
2711+
break;
27382712
}
27392713
}
27402714
}
2741-
if (!resource) {
2742-
resource = {
2743-
type: 'script',
2744-
chunks: [],
2745-
state: NoState,
2746-
props: null,
2747-
};
2748-
resources.scriptsMap.set(key, resource);
2749-
if (__DEV__) {
2750-
markAsRenderedResourceDEV(resource, props);
2751-
}
2752-
// Add to the script flushing queue
2753-
resources.scripts.add(resource);
2754-
2755-
let scriptProps = props;
2756-
const preloadResource = resources.preloadsMap.get(key);
2757-
if (preloadResource) {
2758-
// If we already had a preload we don't want that resource to flush directly.
2759-
// We let the newly created resource govern flushing.
2760-
preloadResource.state |= Blocked;
2761-
scriptProps = {...props};
2762-
adoptPreloadPropsForScriptProps(scriptProps, preloadResource.props);
2763-
}
2764-
// encode the tag as Chunks
2765-
pushScriptImpl(resource.chunks, scriptProps);
2715+
}
2716+
if (!resource) {
2717+
resource = {
2718+
type: 'script',
2719+
chunks: [],
2720+
state: NoState,
2721+
props: null,
2722+
};
2723+
resources.scriptsMap.set(key, resource);
2724+
if (__DEV__) {
2725+
markAsRenderedResourceDEV(resource, props);
2726+
}
2727+
// Add to the script flushing queue
2728+
resources.scripts.add(resource);
2729+
2730+
let scriptProps = props;
2731+
const preloadResource = resources.preloadsMap.get(key);
2732+
if (preloadResource) {
2733+
// If we already had a preload we don't want that resource to flush directly.
2734+
// We let the newly created resource govern flushing.
2735+
preloadResource.state |= Blocked;
2736+
scriptProps = {...props};
2737+
adoptPreloadPropsForScriptProps(scriptProps, preloadResource.props);
27662738
}
2739+
// encode the tag as Chunks
2740+
pushScriptImpl(resource.chunks, scriptProps);
27672741
}
27682742

27692743
if (textEmbedded) {
@@ -4239,9 +4213,6 @@ export function writePreamble(
42394213
resources.scripts.forEach(flushResourceInPreamble, destination);
42404214
resources.scripts.clear();
42414215

4242-
resources.usedScripts.forEach(flushResourceInPreamble, destination);
4243-
resources.usedScripts.clear();
4244-
42454216
resources.explicitStylesheetPreloads.forEach(
42464217
flushResourceInPreamble,
42474218
destination,
@@ -4319,9 +4290,6 @@ export function writeHoistables(
43194290
resources.scripts.forEach(flushResourceLate, destination);
43204291
resources.scripts.clear();
43214292

4322-
resources.usedScripts.forEach(flushResourceLate, destination);
4323-
resources.usedScripts.clear();
4324-
43254293
resources.explicitStylesheetPreloads.forEach(flushResourceLate, destination);
43264294
resources.explicitStylesheetPreloads.clear();
43274295

@@ -4873,7 +4841,6 @@ export type Resources = {
48734841
precedences: Map<string, Set<StyleResource>>,
48744842
stylePrecedences: Map<string, StyleTagResource>,
48754843
scripts: Set<ScriptResource>,
4876-
usedScripts: Set<PreloadResource>,
48774844
explicitStylesheetPreloads: Set<PreloadResource>,
48784845
// explicitImagePreloads: Set<PreloadResource>,
48794846
explicitScriptPreloads: Set<PreloadResource>,
@@ -4900,7 +4867,6 @@ export function createResources(): Resources {
49004867
precedences: new Map(),
49014868
stylePrecedences: new Map(),
49024869
scripts: new Set(),
4903-
usedScripts: new Set(),
49044870
explicitStylesheetPreloads: new Set(),
49054871
// explicitImagePreloads: new Set(),
49064872
explicitScriptPreloads: new Set(),
@@ -5563,19 +5529,6 @@ function preloadAsStylePropsFromProps(href: string, props: any): PreloadProps {
55635529
};
55645530
}
55655531

5566-
function preloadAsScriptPropsFromProps(href: string, props: any): PreloadProps {
5567-
return {
5568-
rel: 'preload',
5569-
as: 'script',
5570-
href,
5571-
crossOrigin: props.crossOrigin,
5572-
fetchPriority: props.fetchPriority,
5573-
integrity: props.integrity,
5574-
nonce: props.nonce,
5575-
referrerPolicy: props.referrerPolicy,
5576-
};
5577-
}
5578-
55795532
function stylesheetPropsFromPreinitOptions(
55805533
href: string,
55815534
precedence: string,
@@ -5694,29 +5647,6 @@ function markAsImperativeResourceDEV(
56945647
}
56955648
}
56965649

5697-
function markAsImplicitResourceDEV(
5698-
resource: Resource,
5699-
underlyingProps: any,
5700-
impliedProps: any,
5701-
): void {
5702-
if (__DEV__) {
5703-
const devResource: ImplicitResourceDEV = (resource: any);
5704-
if (typeof devResource.__provenance === 'string') {
5705-
console.error(
5706-
'Resource already marked for DEV type. This is a bug in React.',
5707-
);
5708-
}
5709-
devResource.__provenance = 'implicit';
5710-
devResource.__underlyingProps = underlyingProps;
5711-
devResource.__impliedProps = impliedProps;
5712-
} else {
5713-
// eslint-disable-next-line react-internal/prod-error-codes
5714-
throw new Error(
5715-
'markAsImplicitResourceDEV was included in a production build. This is a bug in React.',
5716-
);
5717-
}
5718-
}
5719-
57205650
function getAsResourceDEV(
57215651
resource: null | void | Resource,
57225652
): null | ResourceDEV {

0 commit comments

Comments
 (0)