@samitouri / QOS-React-2 / commits / d5736f098e

[Fiber] track stylesheet preloads when explicitly preloaded (#36386)

Previously stylesheet resources would omit connecting with preloads inserted via `preload` which caused unecessary suspension of commits since the stylsheet resource would attempt to load the stylesheet again and delay the initial commit or commit a fallback (depending on whether the current screen should remain). This missing piece is that if you preload a stylesheet you must be able to use that sheets loading state when determining if the stylesheet is already loaded or not. This adds a pending indicator on client inserted prelaod links. We still assume SSR'd preloads are already loaded.

Josh Story committed May 7, 2026 at 10:48 UTC d5736f098edee62c44f27b053e6e48f5fa443803
3 files changed +157 -22
packages/react-dom-bindings/src/client/ReactDOMComponentTree.js
+13
@@ -50,6 +50,7 @@ const internalEventHandlesSetKey = '__reactHandles$' + randomKey;
50 const internalRootNodeResourcesKey = '__reactResources$' + randomKey;
51 const internalHoistableMarker = '__reactMarker$' + randomKey;
52 const internalScrollTimer = '__reactScroll$' + randomKey;
53 +const internalLoadPendingKey = '__reactLoad$' + randomKey;
54
55 type InstanceUnion =
56 | Instance
@@ -386,6 +387,18 @@ export function clearScrollEndTimer(node: EventTarget): void {
387 (node: any)[internalScrollTimer] = undefined;
388 }
389
390 +export function markNodeAsPendingLoad(node: Node): void {
391 + (node: any)[internalLoadPendingKey] = true;
392 +}
393 +
394 +export function clearPendingLoadOnNode(node: Node): void {
395 + (node: any)[internalLoadPendingKey] = undefined;
396 +}
397 +
398 +export function isNodePendingLoad(node: Node): boolean {
399 + return (node: any)[internalLoadPendingKey] === true;
400 +}
401 +
402 export function isOwnedInstance(node: Node): boolean {
403 if (enableInternalInstanceMap) {
404 return !!(
packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
+40 -22
@@ -56,6 +56,9 @@ import {
56 getResourcesFromRoot,
57 isMarkedHoistable,
58 markNodeAsHoistable,
59 + markNodeAsPendingLoad,
60 + clearPendingLoadOnNode,
61 + isNodePendingLoad,
62 isOwnedInstance,
63 } from './ReactDOMComponentTree';
64 import {
@@ -5005,11 +5008,18 @@ function preload(href: string, as: string, options?: ?PreloadImplOptions) {
5008 as === 'script' &&
5009 ownerDocument.querySelector(getScriptSelectorFromKey(key))
5010 ) {
5008 - // We already have a stylesheet for this key. We don't need to preload it.
5011 + // We already have a script for this key. We don't need to preload it.
5012 return;
5013 }
5014 const instance = ownerDocument.createElement('link');
5015 setInitialProperties(instance, 'link', preloadProps);
5016 + if (as === 'style') {
5017 + // Stash a loading state on the preload link. it will clean itself up once settled
5018 + markNodeAsPendingLoad(instance);
5019 + instance.onload = instance.onerror = () => {
5020 + clearPendingLoadOnNode(instance);
5021 + };
5022 + }
5023 markNodeAsHoistable(instance);
5024 (ownerDocument.head: any).appendChild(instance);
5025 }
@@ -5357,19 +5367,16 @@ export function getResource(
5367 resource.instance = instance;
5368 resource.state.loading = Loaded | Inserted;
5369 }
5360 - }
5361 -
5362 - if (!preloadPropsMap.has(key)) {
5363 - const preloadProps = preloadPropsFromStylesheet(qualifiedProps);
5364 - preloadPropsMap.set(key, preloadProps);
5365 - if (!instance) {
5366 - preloadStylesheet(
5367 - ownerDocument,
5368 - key,
5369 - preloadProps,
5370 - resource.state,
5371 - );
5370 + } else {
5371 + // We don't have an instance we need to preload it instead
5372 + // $FlowFixMe[incompatible-type] -- the key we use here can only match non module preloads
5373 + let preloadProps: void | PreloadProps = preloadPropsMap.get(key);
5374 + if (!preloadProps) {
5375 + preloadProps = preloadPropsFromStylesheet(qualifiedProps);
5376 + preloadPropsMap.set(key, preloadProps);
5377 }
5378 +
5379 + preloadStylesheet(ownerDocument, key, preloadProps, resource.state);
5380 }
5381 }
5382 if (currentProps && currentResource === null) {
@@ -5540,22 +5547,33 @@ function preloadStylesheet(
5547 preloadProps: PreloadProps,
5548 state: StylesheetState,
5549 ) {
5543 - const preloadEl = ownerDocument.querySelector(
5550 + let instance = ownerDocument.querySelector(
5551 getPreloadStylesheetSelectorFromKey(key),
5552 );
5546 - if (preloadEl) {
5547 - // If we find a preload already it was SSR'd and we won't have an actual
5548 - // loading state to track. For now we will just assume it is loaded
5549 - state.loading = Loaded;
5553 + if (instance) {
5554 + if (!isNodePendingLoad(instance)) {
5555 + // If we find a preload already it was SSR'd and we won't have an actual
5556 + // loading state to track. For now we will just assume it is loaded
5557 + state.loading = Loaded;
5558 + return;
5559 + } else {
5560 + // fall through and attach loading listeners
5561 + }
5562 } else {
5551 - const instance = ownerDocument.createElement('link');
5552 - state.preload = instance;
5553 - instance.addEventListener('load', () => (state.loading |= Loaded));
5554 - instance.addEventListener('error', () => (state.loading |= Errored));
5563 + instance = ownerDocument.createElement('link');
5564 + markNodeAsPendingLoad(instance);
5565 + instance.onload = instance.onerror = clearPendingLoadOnNode.bind(
5566 + null,
5567 + instance,
5568 + );
5569 setInitialProperties(instance, 'link', preloadProps);
5570 markNodeAsHoistable(instance);
5571 (ownerDocument.head: any).appendChild(instance);
5572 }
5573 + // $FlowFixMe: [incompatible-type] -- if instance is an Element it will also be an HTMLLinkElement
5574 + state.preload = instance;
5575 + instance.addEventListener('load', () => (state.loading |= Loaded));
5576 + instance.addEventListener('error', () => (state.loading |= Errored));
5577 }
5578
5579 function preloadPropsFromStylesheet(
packages/react-dom/src/__tests__/ReactDOMFloat-test.js
+104
@@ -3674,6 +3674,110 @@ body {
3674 );
3675 });
3676
3677 + it('does not suspend a transition on a stylesheet whose preload has already loaded', async () => {
3678 + const root = ReactDOMClient.createRoot(document);
3679 + root.render(
3680 + <html>
3681 + <body>
3682 + <Suspense fallback="loading...">initial</Suspense>
3683 + </body>
3684 + </html>,
3685 + );
3686 + await waitForAll([]);
3687 +
3688 + ReactDOM.preload('route.css', {as: 'style'});
3689 + expect(getMeaningfulChildren(document.head)).toEqual(
3690 + <link rel="preload" href="route.css" as="style" />,
3691 + );
3692 + expect(getMeaningfulChildren(document.body)).toEqual('initial');
3693 +
3694 + loadPreloads(['route.css']);
3695 + assertLog(['load preload: route.css']);
3696 +
3697 + React.startTransition(() => {
3698 + root.render(
3699 + <html>
3700 + <body>
3701 + <Suspense fallback="loading...">
3702 + <link rel="stylesheet" href="route.css" precedence="default" />
3703 + next
3704 + </Suspense>
3705 + </body>
3706 + </html>,
3707 + );
3708 + });
3709 + await waitForAll([]);
3710 +
3711 + expect(getMeaningfulChildren(document.head)).toEqual([
3712 + <link rel="stylesheet" href="route.css" data-precedence="default" />,
3713 + <link rel="preload" href="route.css" as="style" />,
3714 + ]);
3715 + expect(getMeaningfulChildren(document.body)).toEqual('next');
3716 +
3717 + loadStylesheets(['route.css']);
3718 + assertLog(['load stylesheet: route.css']);
3719 + expect(getMeaningfulChildren(document.head)).toEqual([
3720 + <link rel="stylesheet" href="route.css" data-precedence="default" />,
3721 + <link rel="preload" href="route.css" as="style" />,
3722 + ]);
3723 + expect(getMeaningfulChildren(document.body)).toEqual('next');
3724 + });
3725 +
3726 + it('suspends a transition on a stylesheet whose preload has not loaded yet', async () => {
3727 + const root = ReactDOMClient.createRoot(document);
3728 + root.render(
3729 + <html>
3730 + <body>
3731 + <Suspense fallback="loading...">initial</Suspense>
3732 + </body>
3733 + </html>,
3734 + );
3735 + await waitForAll([]);
3736 +
3737 + ReactDOM.preload('route.css', {as: 'style'});
3738 + expect(getMeaningfulChildren(document.head)).toEqual(
3739 + <link rel="preload" href="route.css" as="style" />,
3740 + );
3741 + expect(getMeaningfulChildren(document.body)).toEqual('initial');
3742 +
3743 + React.startTransition(() => {
3744 + root.render(
3745 + <html>
3746 + <body>
3747 + <Suspense fallback="loading...">
3748 + <link rel="stylesheet" href="route.css" precedence="default" />
3749 + next
3750 + </Suspense>
3751 + </body>
3752 + </html>,
3753 + );
3754 + });
3755 + await waitForAll([]);
3756 +
3757 + expect(getMeaningfulChildren(document.head)).toEqual(
3758 + <link rel="preload" href="route.css" as="style" />,
3759 + );
3760 + expect(getMeaningfulChildren(document.body)).toEqual('initial');
3761 +
3762 + loadPreloads(['route.css']);
3763 + assertLog(['load preload: route.css']);
3764 + await waitForAll([]);
3765 + expect(getMeaningfulChildren(document.head)).toEqual([
3766 + <link rel="stylesheet" href="route.css" data-precedence="default" />,
3767 + <link rel="preload" href="route.css" as="style" />,
3768 + ]);
3769 + expect(getMeaningfulChildren(document.body)).toEqual('initial');
3770 +
3771 + loadStylesheets(['route.css']);
3772 + assertLog(['load stylesheet: route.css']);
3773 + await waitForAll([]);
3774 + expect(getMeaningfulChildren(document.head)).toEqual([
3775 + <link rel="stylesheet" href="route.css" data-precedence="default" />,
3776 + <link rel="preload" href="route.css" as="style" />,
3777 + ]);
3778 + expect(getMeaningfulChildren(document.body)).toEqual('next');
3779 + });
3780 +
3781 it('can suspend commits on more than one root for the same resource at the same time', async () => {
3782 document.body.innerHTML = '';
3783 const container1 = document.createElement('div');