Skip to content

Commit 00dbe6a

Browse files
committed
[Float][Fiber] Assume stylesheets in document are already loaded
When we made stylesheets suspend even during high priority updates we exposed a bug in the loading tracking of stylesheets that are loaded as part of the preamble. This allowed these stylesheets to put suspense boundaries into fallback mode more often than expected because cases where a stylesheet was server rendered could now cause a fallback to trigger which was never intended to happen. This fix updates resource construction to evaluate whether the instance exists in the DOM prior to construction and if so marks the resource as loaded and inserted. One ambiguity that needed to be solved still is how to tell whether a stylesheet rendered as part of a late Suspense boundary reveal is already loaded. I updated the instruction to clear out the loading promise after successfully loading. This is useful because later if we encounter this same resource again we can avoid the microtask if it is already loaded. It also means that we can concretely understand that if a stylesheet is in the DOM without this marker then it must have loaded (or errored) already.
1 parent 20841f9 commit 00dbe6a

File tree

5 files changed

+88
-38
lines changed

5 files changed

+88
-38
lines changed

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

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2412,17 +2412,30 @@ export function getResource(
24122412
if (!resource) {
24132413
// We asserted this above but Flow can't figure out that the type satisfies
24142414
const ownerDocument = getDocumentFromRoot(resourceRoot);
2415-
resource = {
2415+
resource = ({
24162416
type: 'stylesheet',
24172417
instance: null,
24182418
count: 0,
24192419
state: {
24202420
loading: NotLoaded,
24212421
preload: null,
24222422
},
2423-
};
2423+
}: StylesheetResource);
24242424
styles.set(key, resource);
2425-
if (!preloadPropsMap.has(key)) {
2425+
const instance = ownerDocument.querySelector(
2426+
getStylesheetSelectorFromKey(key),
2427+
);
2428+
if (instance) {
2429+
const loadingState: ?Promise<mixed> = (instance: any)._p;
2430+
if (loadingState) {
2431+
// This instance is inserted as part of a boundary reveal and is not yet
2432+
// loaded
2433+
} else {
2434+
// This instance is already loaded
2435+
resource.instance = instance;
2436+
resource.state.loading = Loaded | Inserted;
2437+
}
2438+
} else if (!preloadPropsMap.has(key)) {
24262439
preloadStylesheet(
24272440
ownerDocument,
24282441
key,
@@ -2601,26 +2614,21 @@ function preloadStylesheet(
26012614
) {
26022615
preloadPropsMap.set(key, preloadProps);
26032616

2604-
if (!ownerDocument.querySelector(getStylesheetSelectorFromKey(key))) {
2605-
// There is no matching stylesheet instance in the Document.
2606-
// We will insert a preload now to kick off loading because
2607-
// we expect this stylesheet to commit
2608-
const preloadEl = ownerDocument.querySelector(
2609-
getPreloadStylesheetSelectorFromKey(key),
2610-
);
2611-
if (preloadEl) {
2612-
// If we find a preload already it was SSR'd and we won't have an actual
2613-
// loading state to track. For now we will just assume it is loaded
2614-
state.loading = Loaded;
2615-
} else {
2616-
const instance = ownerDocument.createElement('link');
2617-
state.preload = instance;
2618-
instance.addEventListener('load', () => (state.loading |= Loaded));
2619-
instance.addEventListener('error', () => (state.loading |= Errored));
2620-
setInitialProperties(instance, 'link', preloadProps);
2621-
markNodeAsHoistable(instance);
2622-
(ownerDocument.head: any).appendChild(instance);
2623-
}
2617+
const preloadEl = ownerDocument.querySelector(
2618+
getPreloadStylesheetSelectorFromKey(key),
2619+
);
2620+
if (preloadEl) {
2621+
// If we find a preload already it was SSR'd and we won't have an actual
2622+
// loading state to track. For now we will just assume it is loaded
2623+
state.loading = Loaded;
2624+
} else {
2625+
const instance = ownerDocument.createElement('link');
2626+
state.preload = instance;
2627+
instance.addEventListener('load', () => (state.loading |= Loaded));
2628+
instance.addEventListener('error', () => (state.loading |= Errored));
2629+
setInitialProperties(instance, 'link', preloadProps);
2630+
markNodeAsHoistable(instance);
2631+
(ownerDocument.head: any).appendChild(instance);
26242632
}
26252633
}
26262634

packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetExternalRuntime.js

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -80,18 +80,14 @@ export function completeBoundaryWithStyles(
8080
resourceEl.setAttribute(attr, stylesheetDescriptor[j++]);
8181
}
8282
loadingState = resourceEl['_p'] = new Promise((resolve, reject) => {
83-
resourceEl.onload = resolve;
84-
resourceEl.onerror = reject;
83+
resourceEl.onload = cleanupWith.bind(resourceEl, resolve);
84+
resourceEl.onerror = cleanupWith.bind(resourceEl, reject);
8585
});
8686
// Save this resource element so we can bailout if it is used again
8787
resourceMap.set(href, resourceEl);
8888
}
8989
media = resourceEl.getAttribute('media');
90-
if (
91-
loadingState &&
92-
loadingState['s'] !== 'l' &&
93-
(!media || window['matchMedia'](media).matches)
94-
) {
90+
if (loadingState && (!media || window['matchMedia'](media).matches)) {
9591
dependencies.push(loadingState);
9692
}
9793
if (avoidInsert) {
@@ -138,4 +134,9 @@ export function completeBoundaryWithStyles(
138134
);
139135
}
140136

137+
function cleanupWith(cb) {
138+
delete this['_p'];
139+
cb();
140+
}
141+
141142
listenToFormSubmissionsForReplaying();

packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineSource.js

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -82,18 +82,14 @@ export function completeBoundaryWithStyles(
8282
resourceEl.setAttribute(attr, stylesheetDescriptor[j++]);
8383
}
8484
loadingState = resourceEl['_p'] = new Promise((resolve, reject) => {
85-
resourceEl.onload = resolve;
86-
resourceEl.onerror = reject;
85+
resourceEl.onload = cleanupWith.bind(resourceEl, resolve);
86+
resourceEl.onerror = cleanupWith.bind(resourceEl, reject);
8787
});
8888
// Save this resource element so we can bailout if it is used again
8989
resourceMap.set(href, resourceEl);
9090
}
9191
media = resourceEl.getAttribute('media');
92-
if (
93-
loadingState &&
94-
loadingState['s'] !== 'l' &&
95-
(!media || window['matchMedia'](media).matches)
96-
) {
92+
if (loadingState && (!media || window['matchMedia'](media).matches)) {
9793
dependencies.push(loadingState);
9894
}
9995
if (avoidInsert) {
@@ -139,3 +135,8 @@ export function completeBoundaryWithStyles(
139135
),
140136
);
141137
}
138+
139+
function cleanupWith(cb) {
140+
delete this['_p'];
141+
cb();
142+
}

packages/react-dom/src/__tests__/ReactDOMFloat-test.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3348,6 +3348,46 @@ body {
33483348
);
33493349
});
33503350

3351+
it('will assume stylesheets already in the document have loaded if it cannot confirm it is not yet loaded', async () => {
3352+
await act(() => {
3353+
renderToPipeableStream(
3354+
<html>
3355+
<head>
3356+
<link rel="stylesheet" href="foo" data-precedence="default" />
3357+
</head>
3358+
<body>
3359+
<div id="foo" />
3360+
</body>
3361+
</html>,
3362+
).pipe(writable);
3363+
});
3364+
3365+
const root = ReactDOMClient.createRoot(document.querySelector('#foo'));
3366+
3367+
root.render(
3368+
<div>
3369+
<Suspense fallback="loading...">
3370+
<link rel="stylesheet" href="foo" precedence="default" />
3371+
hello world
3372+
</Suspense>
3373+
</div>,
3374+
);
3375+
3376+
await waitForAll([]);
3377+
expect(getMeaningfulChildren(document)).toEqual(
3378+
<html>
3379+
<head>
3380+
<link rel="stylesheet" href="foo" data-precedence="default" />
3381+
</head>
3382+
<body>
3383+
<div id="foo">
3384+
<div>hello world</div>
3385+
</div>
3386+
</body>
3387+
</html>,
3388+
);
3389+
});
3390+
33513391
it('can suspend commits on more than one root for the same resource at the same time', async () => {
33523392
document.body.innerHTML = '';
33533393
const container1 = document.createElement('div');

0 commit comments

Comments
 (0)