@samitouri / QOS-React-2 / commits / 217b2ccf16

[Fiber] render boundary in fallback if it contains a new stylesheet during sync update (#28965)

Updates Suspensey instances and resources to preload even during urgent updates and to potentially suspend. The current implementation is unchanged for transitions but for sync updates if there is a suspense boundary above the resource/instance it will be rendered in fallback mode instead. Note: This behavior is not what we want for images once we make them suspense enabled. We will need to have forked behavior here to distinguish between stylesheets which should never commit when not loaded and images which should commit after a small delay

Josh Story committed May 21, 2024 at 16:03 UTC 217b2ccf160d7594a926d276a75c7312b299be4d
8 files changed +495 -104
packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
+3 -3
@@ -3154,7 +3154,6 @@ export function mayResourceSuspendCommit(resource: Resource): boolean {
3154 }
3155
3156 export function preloadInstance(type: Type, props: Props): boolean {
3157 - // Return true to indicate it's already loaded
3157 return true;
3158 }
3159
@@ -3163,10 +3162,11 @@ export function preloadResource(resource: Resource): boolean {
3162 resource.type === 'stylesheet' &&
3163 (resource.state.loading & Settled) === NotLoaded
3164 ) {
3166 - // we have not finished loading the underlying stylesheet yet.
3165 + // Return false to indicate this resource should suspend
3166 return false;
3167 }
3169 - // Return true to indicate it's already loaded
3168 +
3169 + // Return true to indicate this resource should not suspend
3170 return true;
3171 }
3172
packages/react-dom/src/__tests__/ReactDOMFloat-test.js
+286 -37
@@ -369,11 +369,15 @@ describe('ReactDOMFloat', () => {
369 }
370
371 function loadStylesheets(hrefs) {
372 + loadStylesheetsFrom(document, hrefs);
373 + }
374 +
375 + function loadStylesheetsFrom(root, hrefs) {
376 const event = new window.Event('load');
373 - const nodes = document.querySelectorAll('link[rel="stylesheet"]');
374 - resolveLoadables(hrefs, nodes, event, href =>
375 - Scheduler.log('load stylesheet: ' + href),
376 - );
377 + const nodes = root.querySelectorAll('link[rel="stylesheet"]');
378 + resolveLoadables(hrefs, nodes, event, href => {
379 + Scheduler.log('load stylesheet: ' + href);
380 + });
381 }
382
383 function errorStylesheets(hrefs) {
@@ -832,6 +836,8 @@ describe('ReactDOMFloat', () => {
836 </html>,
837 );
838 await waitForAll([]);
839 + loadPreloads();
840 + await assertLog(['load preload: foo']);
841 expect(getMeaningfulChildren(document)).toEqual(
842 <html>
843 <head>
@@ -849,15 +855,9 @@ describe('ReactDOMFloat', () => {
855 resolveText('bar');
856 });
857 await act(() => {
852 - const sheets = document.querySelectorAll(
853 - 'link[rel="stylesheet"][data-precedence]',
854 - );
855 - const event = document.createEvent('Event');
856 - event.initEvent('load', true, true);
857 - for (let i = 0; i < sheets.length; i++) {
858 - sheets[i].dispatchEvent(event);
859 - }
858 + loadStylesheets();
859 });
860 + await assertLog(['load stylesheet: foo', 'load stylesheet: bar']);
861 expect(getMeaningfulChildren(document)).toEqual(
862 <html>
863 <head>
@@ -877,15 +877,9 @@ describe('ReactDOMFloat', () => {
877 resolveText('foo');
878 });
879 await act(() => {
880 - const sheets = document.querySelectorAll(
881 - 'link[rel="stylesheet"][data-precedence]',
882 - );
883 - const event = document.createEvent('Event');
884 - event.initEvent('load', true, true);
885 - for (let i = 0; i < sheets.length; i++) {
886 - sheets[i].dispatchEvent(event);
887 - }
880 + loadStylesheets();
881 });
882 + await assertLog([]);
883 expect(getMeaningfulChildren(document)).toEqual(
884 <html>
885 <head>
@@ -1128,6 +1122,30 @@ body {
1122 </html>,
1123 );
1124 await waitForAll([]);
1125 + await act(() => {
1126 + loadPreloads();
1127 + loadStylesheets();
1128 + });
1129 + await assertLog([
1130 + 'load preload: one4',
1131 + 'load preload: three4',
1132 + 'load preload: seven1',
1133 + 'load preload: one2',
1134 + 'load preload: two2',
1135 + 'load preload: five1',
1136 + 'load preload: three3',
1137 + 'load preload: four3',
1138 + 'load stylesheet: one1',
1139 + 'load stylesheet: one2',
1140 + 'load stylesheet: one4',
1141 + 'load stylesheet: two2',
1142 + 'load stylesheet: three1',
1143 + 'load stylesheet: three3',
1144 + 'load stylesheet: three4',
1145 + 'load stylesheet: four3',
1146 + 'load stylesheet: five1',
1147 + 'load stylesheet: seven1',
1148 + ]);
1149
1150 expect(getMeaningfulChildren(document)).toEqual(
1151 <html>
@@ -1806,6 +1824,16 @@ body {
1824 </html>,
1825 );
1826 await waitForAll([]);
1827 + await act(() => {
1828 + loadPreloads();
1829 + loadStylesheets();
1830 + });
1831 + await assertLog([
1832 + 'load preload: baz',
1833 + 'load stylesheet: foo',
1834 + 'load stylesheet: baz',
1835 + 'load stylesheet: bar',
1836 + ]);
1837 expect(getMeaningfulChildren(document)).toEqual(
1838 <html>
1839 <head>
@@ -2869,6 +2897,16 @@ body {
2897 </>,
2898 );
2899 await waitForAll([]);
2900 + await act(() => {
2901 + loadPreloads();
2902 + loadStylesheets();
2903 + });
2904 + await assertLog([
2905 + 'load preload: third',
2906 + 'load stylesheet: first',
2907 + 'load stylesheet: second',
2908 + 'load stylesheet: third',
2909 + ]);
2910 expect(getMeaningfulChildren(document)).toEqual(
2911 <html>
2912 <head>
@@ -2946,25 +2984,8 @@ body {
2984 <link rel="preload" as="style" href="bar" />,
2985 ]);
2986
2949 - // Try just this and crash all of Jest
2987 errorStylesheets(['bar']);
2988
2952 - // // Try this and it fails the test when it shouldn't
2953 - // await act(() => {
2954 - // errorStylesheets(['bar']);
2955 - // });
2956 -
2957 - // // Try this there is nothing throwing here which is not really surprising since
2958 - // // the error is bubbling up through some kind of unhandled promise rejection thingy but
2959 - // // still I thought it was worth confirming
2960 - // try {
2961 - // await act(() => {
2962 - // errorStylesheets(['bar']);
2963 - // });
2964 - // } catch (e) {
2965 - // console.log(e);
2966 - // }
2967 -
2989 loadStylesheets(['foo']);
2990 assertLog(['load stylesheet: foo', 'error stylesheet: bar']);
2991
@@ -3127,6 +3148,11 @@ body {
3148 </App>,
3149 );
3150 await waitForAll([]);
3151 + await act(() => {
3152 + loadPreloads(['bar']);
3153 + loadStylesheets(['bar']);
3154 + });
3155 + await assertLog(['load preload: bar', 'load stylesheet: bar']);
3156
3157 // The bar stylesheet was inserted. There's still a "foo" preload, even
3158 // though that update was superseded.
@@ -3197,6 +3223,131 @@ body {
3223 );
3224 });
3225
3226 + it('will put a Suspense boundary into fallback if it contains a stylesheet not loaded during a sync update', async () => {
3227 + function App({children}) {
3228 + return (
3229 + <html>
3230 + <body>{children}</body>
3231 + </html>
3232 + );
3233 + }
3234 + const root = ReactDOMClient.createRoot(document);
3235 +
3236 + await clientAct(() => {
3237 + root.render(<App />);
3238 + });
3239 + await waitForAll([]);
3240 +
3241 + await clientAct(() => {
3242 + root.render(
3243 + <App>
3244 + <Suspense fallback="loading...">
3245 + <div>
3246 + hello
3247 + <link rel="stylesheet" href="foo" precedence="default" />
3248 + </div>
3249 + </Suspense>
3250 + </App>,
3251 + );
3252 + });
3253 + await waitForAll([]);
3254 +
3255 + // Although the commit suspended, a preload was inserted.
3256 + expect(getMeaningfulChildren(document)).toEqual(
3257 + <html>
3258 + <head>
3259 + <link rel="preload" href="foo" as="style" />
3260 + </head>
3261 + <body>loading...</body>
3262 + </html>,
3263 + );
3264 +
3265 + loadPreloads(['foo']);
3266 + assertLog(['load preload: foo']);
3267 + expect(getMeaningfulChildren(document)).toEqual(
3268 + <html>
3269 + <head>
3270 + <link rel="stylesheet" href="foo" data-precedence="default" />
3271 + <link rel="preload" href="foo" as="style" />
3272 + </head>
3273 + <body>loading...</body>
3274 + </html>,
3275 + );
3276 +
3277 + loadStylesheets(['foo']);
3278 + assertLog(['load stylesheet: foo']);
3279 + expect(getMeaningfulChildren(document)).toEqual(
3280 + <html>
3281 + <head>
3282 + <link rel="stylesheet" href="foo" data-precedence="default" />
3283 + <link rel="preload" href="foo" as="style" />
3284 + </head>
3285 + <body>
3286 + <div>hello</div>
3287 + </body>
3288 + </html>,
3289 + );
3290 +
3291 + await clientAct(() => {
3292 + root.render(
3293 + <App>
3294 + <Suspense fallback="loading...">
3295 + <div>
3296 + hello
3297 + <link rel="stylesheet" href="foo" precedence="default" />
3298 + <link rel="stylesheet" href="bar" precedence="default" />
3299 + </div>
3300 + </Suspense>
3301 + </App>,
3302 + );
3303 + });
3304 + await waitForAll([]);
3305 + expect(getMeaningfulChildren(document)).toEqual(
3306 + <html>
3307 + <head>
3308 + <link rel="stylesheet" href="foo" data-precedence="default" />
3309 + <link rel="preload" href="foo" as="style" />
3310 + <link rel="preload" href="bar" as="style" />
3311 + </head>
3312 + <body>
3313 + <div style="display: none;">hello</div>loading...
3314 + </body>
3315 + </html>,
3316 + );
3317 +
3318 + loadPreloads(['bar']);
3319 + assertLog(['load preload: bar']);
3320 + expect(getMeaningfulChildren(document)).toEqual(
3321 + <html>
3322 + <head>
3323 + <link rel="stylesheet" href="foo" data-precedence="default" />
3324 + <link rel="stylesheet" href="bar" data-precedence="default" />
3325 + <link rel="preload" href="foo" as="style" />
3326 + <link rel="preload" href="bar" as="style" />
3327 + </head>
3328 + <body>
3329 + <div style="display: none;">hello</div>loading...
3330 + </body>
3331 + </html>,
3332 + );
3333 +
3334 + loadStylesheets(['bar']);
3335 + assertLog(['load stylesheet: bar']);
3336 + expect(getMeaningfulChildren(document)).toEqual(
3337 + <html>
3338 + <head>
3339 + <link rel="stylesheet" href="foo" data-precedence="default" />
3340 + <link rel="stylesheet" href="bar" data-precedence="default" />
3341 + <link rel="preload" href="foo" as="style" />
3342 + <link rel="preload" href="bar" as="style" />
3343 + </head>
3344 + <body>
3345 + <div style="">hello</div>
3346 + </body>
3347 + </html>,
3348 + );
3349 + });
3350 +
3351 it('can suspend commits on more than one root for the same resource at the same time', async () => {
3352 document.body.innerHTML = '';
3353 const container1 = document.createElement('div');
@@ -6465,6 +6616,14 @@ body {
6616 </html>,
6617 );
6618 await waitForAll([]);
6619 + await act(() => {
6620 + loadPreloads();
6621 + loadStylesheets();
6622 + });
6623 + await assertLog([
6624 + 'load preload: aresource',
6625 + 'load stylesheet: aresource',
6626 + ]);
6627
6628 expect(getMeaningfulChildren(document)).toEqual(
6629 <html>
@@ -6556,6 +6715,22 @@ body {
6715 </html>,
6716 );
6717 await waitForAll([]);
6718 + await act(() => {
6719 + loadPreloads();
6720 + loadStylesheets();
6721 + });
6722 + await assertLog([
6723 + 'load preload: bar1',
6724 + 'load preload: foo3',
6725 + 'load preload: default2',
6726 + 'load stylesheet: foo1',
6727 + 'load stylesheet: foo2',
6728 + 'load stylesheet: foo3',
6729 + 'load stylesheet: default1',
6730 + 'load stylesheet: default2',
6731 + 'load stylesheet: bar1',
6732 + ]);
6733 +
6734 expect(getMeaningfulChildren(document)).toEqual(
6735 <html>
6736 <head>
@@ -6590,6 +6765,11 @@ body {
6765 </html>,
6766 );
6767 await waitForAll([]);
6768 + await act(() => {
6769 + loadPreloads();
6770 + loadStylesheets();
6771 + });
6772 + await assertLog(['load preload: foo', 'load stylesheet: foo']);
6773
6774 root.render(
6775 <html>
@@ -6640,6 +6820,17 @@ body {
6820 },
6821 );
6822 await waitForAll([]);
6823 + await act(() => {
6824 + loadPreloads();
6825 + loadStylesheets();
6826 + });
6827 + await assertLog([
6828 + 'load preload: qux',
6829 + 'load stylesheet: foo',
6830 + 'load stylesheet: bar',
6831 + 'load stylesheet: qux',
6832 + ]);
6833 +
6834 expect(getMeaningfulChildren(document)).toEqual(
6835 <html>
6836 <head>
@@ -6666,6 +6857,16 @@ body {
6857 </html>,
6858 );
6859 await waitForAll([]);
6860 + await act(() => {
6861 + loadPreloads();
6862 + loadStylesheets();
6863 + });
6864 + await assertLog([
6865 + 'load preload: foo',
6866 + 'load preload: bar',
6867 + 'load stylesheet: foo',
6868 + 'load stylesheet: bar',
6869 + ]);
6870 expect(getMeaningfulChildren(document)).toEqual(
6871 <html>
6872 <head>
@@ -6688,6 +6889,12 @@ body {
6889 </html>,
6890 );
6891 await waitForAll([]);
6892 + await act(() => {
6893 + loadPreloads();
6894 + loadStylesheets();
6895 + });
6896 + await assertLog(['load preload: baz', 'load stylesheet: baz']);
6897 +
6898 // The reason we do not see preloads in the head is they are inserted synchronously
6899 // during render and then when the new singleton mounts it resets it's content, retaining only styles
6900 expect(getMeaningfulChildren(document)).toEqual(
@@ -6702,6 +6909,7 @@ body {
6909 </html>,
6910 );
6911 });
6912 +
6913 it('can support styles inside portals to a shadowRoot', async () => {
6914 const shadow = document.body.attachShadow({mode: 'open'});
6915 const root = ReactDOMClient.createRoot(container);
@@ -6724,6 +6932,16 @@ body {
6932 </>,
6933 );
6934 await waitForAll([]);
6935 + await act(() => {
6936 + loadPreloads();
6937 + loadStylesheets();
6938 + loadStylesheetsFrom(shadow);
6939 + });
6940 + await assertLog([
6941 + 'load preload: foo',
6942 + 'load stylesheet: foo',
6943 + 'load stylesheet: foo',
6944 + ]);
6945 expect(getMeaningfulChildren(document)).toEqual(
6946 <html>
6947 <head>
@@ -6745,6 +6963,7 @@ body {
6963 <div>shadow</div>,
6964 ]);
6965 });
6966 +
6967 it('can support styles inside portals to an element in shadowRoots', async () => {
6968 const template = document.createElement('template');
6969 template.innerHTML =
@@ -6783,6 +7002,24 @@ body {
7002 </>,
7003 );
7004 await waitForAll([]);
7005 + await act(() => {
7006 + loadPreloads();
7007 + loadStylesheets();
7008 + loadStylesheetsFrom(shadow);
7009 + loadStylesheetsFrom(shadowContainer2);
7010 + loadStylesheetsFrom(shadowContainer2);
7011 + });
7012 + await assertLog([
7013 + 'load preload: foo',
7014 + 'load preload: bar',
7015 + 'load preload: baz',
7016 + 'load preload: qux',
7017 + 'load stylesheet: foo',
7018 + 'load stylesheet: foo',
7019 + 'load stylesheet: baz',
7020 + 'load stylesheet: bar',
7021 + 'load stylesheet: qux',
7022 + ]);
7023 expect(getMeaningfulChildren(document)).toEqual(
7024 <html>
7025 <head>
@@ -6854,6 +7091,18 @@ body {
7091 }
7092 root.render(<ClientApp />);
7093 await waitForAll([]);
7094 + await act(() => {
7095 + loadPreloads();
7096 + loadStylesheets();
7097 + });
7098 + await assertLog([
7099 + 'load preload: preload',
7100 + 'load preload: with\nnewline',
7101 + 'load preload: style"][rel="stylesheet',
7102 + 'load stylesheet: style',
7103 + 'load stylesheet: with\\slashes',
7104 + 'load stylesheet: style"][rel="stylesheet',
7105 + ]);
7106 expect(getMeaningfulChildren(document)).toEqual(
7107 <html>
7108 <head>
packages/react-native-renderer/src/ReactFiberConfigNative.js
+1 -1
@@ -535,7 +535,7 @@ export function maySuspendCommit(type: Type, props: Props): boolean {
535 }
536
537 export function preloadInstance(type: Type, props: Props): boolean {
538 - // Return true to indicate it's already loaded
538 + // Return false to indicate it's already loaded
539 return true;
540 }
541
packages/react-noop-renderer/src/createReactNoop.js
+1 -2
@@ -611,12 +611,11 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
611 }
612 return false;
613 } else {
614 - // If this is false, React will trigger a fallback, if needed.
614 return record.status === 'fulfilled';
615 }
616 },
617
619 - preloadResource(resource: mixed): boolean {
618 + preloadResource(resource: mixed): number {
619 throw new Error(
620 'Resources are not implemented for React Noop yet. This method should not be called',
621 );
packages/react-reconciler/src/ReactFiberCompleteWork.js
+15 -48
@@ -152,7 +152,6 @@ import {
152 getRenderTargetTime,
153 getWorkInProgressTransitions,
154 shouldRemainOnPreviousScreen,
155 - getWorkInProgressRootRenderLanes,
155 } from './ReactFiberWorkLoop';
156 import {
157 OffscreenLane,
@@ -161,7 +160,6 @@ import {
160 includesSomeLane,
161 mergeLanes,
162 claimNextRetryLane,
164 - includesOnlyNonUrgentLanes,
163 } from './ReactFiberLane';
164 import {resetChildFibers} from './ReactChildFiber';
165 import {createScopeInstance} from './ReactFiberScope';
@@ -534,41 +532,15 @@ function preloadInstanceAndSuspendIfNeeded(
532 // loaded yet.
533 workInProgress.flags |= MaySuspendCommit;
534
537 - // Check if we're rendering at a "non-urgent" priority. This is the same
538 - // check that `useDeferredValue` does to determine whether it needs to
539 - // defer. This is partly for gradual adoption purposes (i.e. shouldn't start
540 - // suspending until you opt in with startTransition or Suspense) but it
541 - // also happens to be the desired behavior for the concrete use cases we've
542 - // thought of so far, like CSS loading, fonts, images, etc.
543 - //
544 - // We check the "root" render lanes here rather than the "subtree" render
545 - // because during a retry or offscreen prerender, the "subtree" render
546 - // lanes may include additional "base" lanes that were deferred during
547 - // a previous render.
548 - // TODO: We may decide to expose a way to force a fallback even during a
549 - // sync update.
550 - const rootRenderLanes = getWorkInProgressRootRenderLanes();
551 - if (!includesOnlyNonUrgentLanes(rootRenderLanes)) {
552 - // This is an urgent render. Don't suspend or show a fallback. Also,
553 - // there's no need to preload, because we're going to commit this
554 - // synchronously anyway.
555 - // TODO: Could there be benefit to preloading even during a synchronous
556 - // render? The main thread will be blocked until the commit phase, but
557 - // maybe the browser would be able to start loading off thread anyway?
558 - // Likely a micro-optimization either way because typically new content
559 - // is loaded during a transition, not an urgent render.
560 - } else {
561 - // Preload the instance
562 - const isReady = preloadInstance(type, props);
563 - if (!isReady) {
564 - if (shouldRemainOnPreviousScreen()) {
565 - // It's OK to suspend. Mark the fiber so we know to suspend before the
566 - // commit phase. Then continue rendering.
567 - workInProgress.flags |= ShouldSuspendCommit;
568 - } else {
569 - // Trigger a fallback rather than block the render.
570 - suspendCommit();
571 - }
535 + // preload the instance if necessary. Even if this is an urgent render there
536 + // could be benefits to preloading early.
537 + // @TODO we should probably do the preload in begin work
538 + const isReady = preloadInstance(type, props);
539 + if (!isReady) {
540 + if (shouldRemainOnPreviousScreen()) {
541 + workInProgress.flags |= ShouldSuspendCommit;
542 + } else {
543 + suspendCommit();
544 }
545 }
546 }
@@ -588,17 +560,12 @@ function preloadResourceAndSuspendIfNeeded(
560
561 workInProgress.flags |= MaySuspendCommit;
562
591 - const rootRenderLanes = getWorkInProgressRootRenderLanes();
592 - if (!includesOnlyNonUrgentLanes(rootRenderLanes)) {
593 - // This is an urgent render. Don't suspend or show a fallback.
594 - } else {
595 - const isReady = preloadResource(resource);
596 - if (!isReady) {
597 - if (shouldRemainOnPreviousScreen()) {
598 - workInProgress.flags |= ShouldSuspendCommit;
599 - } else {
600 - suspendCommit();
601 - }
563 + const isReady = preloadResource(resource);
564 + if (!isReady) {
565 + if (shouldRemainOnPreviousScreen()) {
566 + workInProgress.flags |= ShouldSuspendCommit;
567 + } else {
568 + suspendCommit();
569 }
570 }
571 }
packages/react-reconciler/src/ReactFiberWorkLoop.js
+12 -4
@@ -23,6 +23,7 @@ import type {
23 } from './ReactFiberTracingMarkerComponent';
24 import type {OffscreenInstance} from './ReactFiberActivityComponent';
25 import type {RenderTaskFn} from './ReactFiberRootScheduler';
26 +import type {Resource} from './ReactFiberConfig';
27
28 import {
29 enableCreateEventHandleAPI,
@@ -75,6 +76,7 @@ import {
76 startSuspendingCommit,
77 waitForCommitToBeReady,
78 preloadInstance,
79 + preloadResource,
80 supportsHydration,
81 setCurrentUpdatePriority,
82 getCurrentUpdatePriority,
@@ -123,6 +125,7 @@ import {
125 MountPassiveDev,
126 MountLayoutDev,
127 DidDefer,
128 + ShouldSuspendCommit,
129 } from './ReactFiberFlags';
130 import {
131 NoLanes,
@@ -151,7 +154,6 @@ import {
154 movePendingFibersToMemoized,
155 addTransitionToLanesMap,
156 getTransitionsForLanes,
154 - includesOnlyNonUrgentLanes,
157 includesSomeLane,
158 OffscreenLane,
159 SyncUpdateLanes,
@@ -1182,7 +1184,7 @@ function commitRootWhenReady(
1184 ) {
1185 // TODO: Combine retry throttling with Suspensey commits. Right now they run
1186 // one after the other.
1185 - if (includesOnlyNonUrgentLanes(lanes)) {
1187 + if (finishedWork.subtreeFlags & ShouldSuspendCommit) {
1188 // Before committing, ask the renderer whether the host tree is ready.
1189 // If it's not, we'll wait until it notifies us.
1190 startSuspendingCommit();
@@ -2219,9 +2221,13 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
2221 break;
2222 }
2223 case SuspendedOnInstanceAndReadyToContinue: {
2224 + let resource: null | Resource = null;
2225 switch (workInProgress.tag) {
2226 + case HostHoistable: {
2227 + resource = workInProgress.memoizedState;
2228 + }
2229 + // intentional fallthrough
2230 case HostComponent:
2224 - case HostHoistable:
2231 case HostSingleton: {
2232 // Before unwinding the stack, check one more time if the
2233 // instance is ready. It may have loaded when React yielded to
@@ -2232,7 +2238,9 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
2238 const hostFiber = workInProgress;
2239 const type = hostFiber.type;
2240 const props = hostFiber.pendingProps;
2235 - const isReady = preloadInstance(type, props);
2241 + const isReady = resource
2242 + ? preloadResource(resource)
2243 + : preloadInstance(type, props);
2244 if (isReady) {
2245 // The data resolved. Resume the work loop as if nothing
2246 // suspended. Unlike when a user component suspends, we don't
packages/react-reconciler/src/__tests__/ReactSuspenseyCommitPhase-test.js
+176 -8
@@ -44,6 +44,7 @@ describe('ReactSuspenseyCommitPhase', () => {
44 return (
45 <suspensey-thing
46 src={src}
47 + timeout={100}
48 onLoadStart={() => Scheduler.log(`Image requested [${src}]`)}
49 />
50 );
@@ -104,7 +105,49 @@ describe('ReactSuspenseyCommitPhase', () => {
105 expect(root).toMatchRenderedOutput(<suspensey-thing src="B" />);
106 });
107
107 - test('does not suspend commit during urgent update', async () => {
108 + test('suspend commit during initial mount at the root', async () => {
109 + const root = ReactNoop.createRoot();
110 + await act(async () => {
111 + startTransition(() => {
112 + root.render(<SuspenseyImage src="A" />);
113 + });
114 + });
115 + assertLog(['Image requested [A]']);
116 + expect(getSuspenseyThingStatus('A')).toBe('pending');
117 + expect(root).toMatchRenderedOutput(null);
118 +
119 + resolveSuspenseyThing('A');
120 + expect(getSuspenseyThingStatus('A')).toBe('fulfilled');
121 + expect(root).toMatchRenderedOutput(<suspensey-thing src="A" />);
122 + });
123 +
124 + test('suspend commit during update at the root', async () => {
125 + const root = ReactNoop.createRoot();
126 + await act(() => resolveSuspenseyThing('A'));
127 + expect(getSuspenseyThingStatus('A')).toBe('fulfilled');
128 + expect(root).toMatchRenderedOutput(null);
129 + await act(async () => {
130 + startTransition(() => {
131 + root.render(<SuspenseyImage src="A" />);
132 + });
133 + });
134 + expect(root).toMatchRenderedOutput(<suspensey-thing src="A" />);
135 +
136 + await act(async () => {
137 + startTransition(() => {
138 + root.render(<SuspenseyImage src="B" />);
139 + });
140 + });
141 + assertLog(['Image requested [B]']);
142 + expect(getSuspenseyThingStatus('B')).toBe('pending');
143 + expect(root).toMatchRenderedOutput(<suspensey-thing src="A" />);
144 +
145 + resolveSuspenseyThing('B');
146 + expect(getSuspenseyThingStatus('B')).toBe('fulfilled');
147 + expect(root).toMatchRenderedOutput(<suspensey-thing src="B" />);
148 + });
149 +
150 + test('suspend commit during urgent initial mount', async () => {
151 const root = ReactNoop.createRoot();
152 await act(async () => {
153 root.render(
@@ -113,16 +156,141 @@ describe('ReactSuspenseyCommitPhase', () => {
156 </Suspense>,
157 );
158 });
116 - // We intentionally don't preload during an urgent update because the
117 - // resource will be inserted synchronously, anyway.
118 - // TODO: Maybe we should, though? Could be that the browser is able to start
119 - // the preload in background even though the main thread is blocked. Likely
120 - // a micro-optimization either way because typically new content is loaded
121 - // during a transition, not an urgent render.
122 - expect(getSuspenseyThingStatus('A')).toBe(null);
159 + assertLog(['Image requested [A]', 'Loading...']);
160 + expect(getSuspenseyThingStatus('A')).toBe('pending');
161 + expect(root).toMatchRenderedOutput('Loading...');
162 +
163 + resolveSuspenseyThing('A');
164 + expect(getSuspenseyThingStatus('A')).toBe('fulfilled');
165 + expect(root).toMatchRenderedOutput(<suspensey-thing src="A" />);
166 + });
167 +
168 + test('suspend commit during urgent update', async () => {
169 + const root = ReactNoop.createRoot();
170 + await act(() => resolveSuspenseyThing('A'));
171 + expect(getSuspenseyThingStatus('A')).toBe('fulfilled');
172 + await act(async () => {
173 + root.render(
174 + <Suspense fallback={<Text text="Loading..." />}>
175 + <SuspenseyImage src="A" />
176 + </Suspense>,
177 + );
178 + });
179 + expect(root).toMatchRenderedOutput(<suspensey-thing src="A" />);
180 +
181 + resolveSuspenseyThing('A');
182 + expect(getSuspenseyThingStatus('A')).toBe('fulfilled');
183 + expect(root).toMatchRenderedOutput(<suspensey-thing src="A" />);
184 +
185 + await act(async () => {
186 + root.render(
187 + <Suspense fallback={<Text text="Loading..." />}>
188 + <SuspenseyImage src="B" />
189 + </Suspense>,
190 + );
191 + });
192 + assertLog(['Image requested [B]', 'Loading...']);
193 + expect(getSuspenseyThingStatus('B')).toBe('pending');
194 + expect(root).toMatchRenderedOutput(
195 + <>
196 + <suspensey-thing src="A" hidden={true} />
197 + {'Loading...'}
198 + </>,
199 + );
200 +
201 + resolveSuspenseyThing('B');
202 + expect(getSuspenseyThingStatus('B')).toBe('fulfilled');
203 + expect(root).toMatchRenderedOutput(<suspensey-thing src="B" />);
204 + });
205 +
206 + test('suspends commit during urgent initial mount at the root', async () => {
207 + const root = ReactNoop.createRoot();
208 + await act(async () => {
209 + root.render(<SuspenseyImage src="A" />);
210 + });
211 + assertLog(['Image requested [A]']);
212 + expect(getSuspenseyThingStatus('A')).toBe('pending');
213 + expect(root).toMatchRenderedOutput(null);
214 +
215 + resolveSuspenseyThing('A');
216 + expect(getSuspenseyThingStatus('A')).toBe('fulfilled');
217 + expect(root).toMatchRenderedOutput(<suspensey-thing src="A" />);
218 + });
219 +
220 + test('suspends commit during urgent update at the root', async () => {
221 + const root = ReactNoop.createRoot();
222 + await act(() => resolveSuspenseyThing('A'));
223 + expect(getSuspenseyThingStatus('A')).toBe('fulfilled');
224 + expect(root).toMatchRenderedOutput(null);
225 + await act(async () => {
226 + root.render(<SuspenseyImage src="A" />);
227 + });
228 + expect(root).toMatchRenderedOutput(<suspensey-thing src="A" />);
229 +
230 + await act(async () => {
231 + root.render(<SuspenseyImage src="B" />);
232 + });
233 + assertLog(['Image requested [B]']);
234 + expect(getSuspenseyThingStatus('B')).toBe('pending');
235 + expect(root).toMatchRenderedOutput(<suspensey-thing src="A" />);
236 +
237 + resolveSuspenseyThing('B');
238 + expect(getSuspenseyThingStatus('B')).toBe('fulfilled');
239 + expect(root).toMatchRenderedOutput(<suspensey-thing src="B" />);
240 + });
241 +
242 + // @TODO This isn't actually ideal behavior. We would really want the commit to suspend
243 + // even if it is forced to be sync because we don't want to FOUC but refactoring the sync
244 + // pathway is too risky to land right now so we just accept that we can still FOUC in this
245 + // very specific case.
246 + test('does not suspend commit during urgent initial mount at the root when sync rendering', async () => {
247 + const root = ReactNoop.createRoot();
248 + await act(async () => {
249 + ReactNoop.flushSync(() => {
250 + root.render(<SuspenseyImage src="A" />);
251 + });
252 + });
253 + assertLog(['Image requested [A]']);
254 + expect(getSuspenseyThingStatus('A')).toBe('pending');
255 + // We would expect this to be null if we did in fact suspend this commit
256 + expect(root).toMatchRenderedOutput(<suspensey-thing src="A" />);
257 +
258 + resolveSuspenseyThing('A');
259 + expect(getSuspenseyThingStatus('A')).toBe('fulfilled');
260 expect(root).toMatchRenderedOutput(<suspensey-thing src="A" />);
261 });
262
263 + // @TODO This isn't actually ideal behavior. We would really want the commit to suspend
264 + // even if it is forced to be sync because we don't want to FOUC but refactoring the sync
265 + // pathway is too risky to land right now so we just accept that we can still FOUC in this
266 + // very specific case.
267 + test('does not suspend commit during urgent update at the root when sync rendering', async () => {
268 + const root = ReactNoop.createRoot();
269 + await act(() => resolveSuspenseyThing('A'));
270 + expect(getSuspenseyThingStatus('A')).toBe('fulfilled');
271 + expect(root).toMatchRenderedOutput(null);
272 + await act(async () => {
273 + ReactNoop.flushSync(() => {
274 + root.render(<SuspenseyImage src="A" />);
275 + });
276 + });
277 + expect(root).toMatchRenderedOutput(<suspensey-thing src="A" />);
278 +
279 + await act(async () => {
280 + ReactNoop.flushSync(() => {
281 + root.render(<SuspenseyImage src="B" />);
282 + });
283 + });
284 + assertLog(['Image requested [B]']);
285 + expect(getSuspenseyThingStatus('B')).toBe('pending');
286 + // We would expect this to be hidden if we did in fact suspend this commit
287 + expect(root).toMatchRenderedOutput(<suspensey-thing src="B" />);
288 +
289 + resolveSuspenseyThing('B');
290 + expect(getSuspenseyThingStatus('B')).toBe('fulfilled');
291 + expect(root).toMatchRenderedOutput(<suspensey-thing src="B" />);
292 + });
293 +
294 test('an urgent update interrupts a suspended commit', async () => {
295 const root = ReactNoop.createRoot();
296
packages/shared/forks/ReactFeatureFlags.www-dynamic.js
+1 -1
@@ -19,7 +19,7 @@ export const forceConcurrentByDefaultForTesting = __VARIANT__;
19 export const enableUnifiedSyncLane = __VARIANT__;
20 export const enableTransitionTracing = __VARIANT__;
21 export const enableDeferRootSchedulingToMicrotask = __VARIANT__;
22 -export const alwaysThrottleRetries = __VARIANT__;
22 +export const alwaysThrottleRetries = true;
23 export const enableDO_NOT_USE_disableStrictPassiveEffect = __VARIANT__;
24 export const enableUseDeferredValueInitialArg = __VARIANT__;
25 export const enableRenderableContext = __VARIANT__;