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

Add Suspensey Images behind a Flag (#32819)

We've known we've wanted this for many years and most of the implementation was already done for Suspensey CSS. This waits to commit until images have decoded by default or up to 500ms timeout (same as suspensey fonts). It only applies to Transitions, Retries (Suspense), Gesture Transitions (flag) and Idle (doesn't exist). Sync updates just commit immediately. `<img loading="lazy" src="..." />` opts out since you explicitly want it to load lazily in that case. `<img onLoad={...} src="..." />` also opts out since that implies you're ok with managing your own reveal. In the future, we may add an opt in e.g. `<img blocking="render" src="..." />` that opts into longer timeouts and re-suspends even sync updates. Perhaps also triggering error boundaries on errors. The rollout for this would have to go in a major and we may have to relax the default timeout to not delay too much by default. However, we can also make this part of `enableViewTransition` so that if you opt-in by using View Transitions then those animations will suspend on images. That we could ship in a minor.

Sebastian Markbåge committed Apr 4, 2025 at 14:54 UTC efb22d8850382c3b53c1b2b8d22036d7e6cc9488
21 files changed +301 -48
fixtures/view-transition/src/components/Page.js
+6
@@ -41,6 +41,12 @@ function Component() {
41 transitions['enter-slide-right'] + ' ' + transitions['exit-slide-left']
42 }>
43 <p className="roboto-font">Slide In from Left, Slide Out to Right</p>
44 + <p>
45 + <img
46 + src="https://react.dev/_next/image?url=%2Fimages%2Fteam%2Fsebmarkbage.jpg&w=3840&q=75"
47 + width="300"
48 + />
49 + </p>
50 </ViewTransition>
51 );
52 }
packages/react-art/src/ReactFiberConfigART.js
+9 -1
@@ -596,6 +596,14 @@ export function maySuspendCommit(type, props) {
596 return false;
597 }
598
599 +export function maySuspendCommitOnUpdate(type, oldProps, newProps) {
600 + return false;
601 +}
602 +
603 +export function maySuspendCommitInSyncRender(type, props) {
604 + return false;
605 +}
606 +
607 export function preloadInstance(type, props) {
608 // Return true to indicate it's already loaded
609 return true;
@@ -603,7 +611,7 @@ export function preloadInstance(type, props) {
611
612 export function startSuspendingCommit() {}
613
606 -export function suspendInstance(type, props) {}
614 +export function suspendInstance(instance, type, props) {}
615
616 export function suspendOnActiveViewTransition(container) {}
617
packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
+80 -6
@@ -103,6 +103,7 @@ import {
103 disableLegacyMode,
104 enableMoveBefore,
105 disableCommentsAsDOMContainers,
106 + enableSuspenseyImages,
107 } from 'shared/ReactFeatureFlags';
108 import {
109 HostComponent,
@@ -145,6 +146,10 @@ export type Props = {
146 is?: string,
147 size?: number,
148 multiple?: boolean,
149 + src?: string,
150 + srcSet?: string,
151 + loading?: 'eager' | 'lazy',
152 + onLoad?: (event: any) => void,
153 ...
154 };
155 type RawProps = {
@@ -769,9 +774,9 @@ export function commitMount(
774 // only need to assign one. And Safari just never triggers a new load event which means this technique
775 // is already a noop regardless of which properties are assigned. We should revisit if browsers update
776 // this heuristic in the future.
772 - if ((newProps: any).src) {
777 + if (newProps.src) {
778 ((domElement: any): HTMLImageElement).src = (newProps: any).src;
774 - } else if ((newProps: any).srcSet) {
779 + } else if (newProps.srcSet) {
780 ((domElement: any): HTMLImageElement).srcset = (newProps: any).srcSet;
781 }
782 return;
@@ -4974,6 +4979,36 @@ export function isHostHoistableType(
4979 }
4980
4981 export function maySuspendCommit(type: Type, props: Props): boolean {
4982 + if (!enableSuspenseyImages) {
4983 + return false;
4984 + }
4985 + // Suspensey images are the default, unless you opt-out of with either
4986 + // loading="lazy" or onLoad={...} which implies you're ok waiting.
4987 + return (
4988 + type === 'img' &&
4989 + props.src != null &&
4990 + props.src !== '' &&
4991 + props.onLoad == null &&
4992 + props.loading !== 'lazy'
4993 + );
4994 +}
4995 +
4996 +export function maySuspendCommitOnUpdate(
4997 + type: Type,
4998 + oldProps: Props,
4999 + newProps: Props,
5000 +): boolean {
5001 + return (
5002 + maySuspendCommit(type, newProps) &&
5003 + (newProps.src !== oldProps.src || newProps.srcSet !== oldProps.srcSet)
5004 + );
5005 +}
5006 +
5007 +export function maySuspendCommitInSyncRender(
5008 + type: Type,
5009 + props: Props,
5010 +): boolean {
5011 + // TODO: Allow sync lanes to suspend too with an opt-in.
5012 return false;
5013 }
5014
@@ -4984,8 +5019,17 @@ export function mayResourceSuspendCommit(resource: Resource): boolean {
5019 );
5020 }
5021
4987 -export function preloadInstance(type: Type, props: Props): boolean {
4988 - return true;
5022 +export function preloadInstance(
5023 + instance: Instance,
5024 + type: Type,
5025 + props: Props,
5026 +): boolean {
5027 + // We don't need to preload Suspensey images because the browser will
5028 + // load them early once we set the src.
5029 + // If we return true here, we'll still get a suspendInstance call in the
5030 + // pre-commit phase to determine if we still need to decode the image or
5031 + // if was dropped from cache. This just avoids rendering Suspense fallback.
5032 + return !!(instance: any).complete;
5033 }
5034
5035 export function preloadResource(resource: Resource): boolean {
@@ -5022,8 +5066,38 @@ export function startSuspendingCommit(): void {
5066 };
5067 }
5068
5025 -export function suspendInstance(type: Type, props: Props): void {
5026 - return;
5069 +const SUSPENSEY_IMAGE_TIMEOUT = 500;
5070 +
5071 +export function suspendInstance(
5072 + instance: Instance,
5073 + type: Type,
5074 + props: Props,
5075 +): void {
5076 + if (!enableSuspenseyImages) {
5077 + return;
5078 + }
5079 + if (suspendedState === null) {
5080 + throw new Error(
5081 + 'Internal React Error: suspendedState null when it was expected to exists. Please report this as a React bug.',
5082 + );
5083 + }
5084 + const state = suspendedState;
5085 + if (
5086 + // $FlowFixMe[prop-missing]
5087 + typeof instance.decode === 'function' &&
5088 + typeof setTimeout === 'function'
5089 + ) {
5090 + // If this browser supports decode() API, we use it to suspend waiting on the image.
5091 + // The loading should have already started at this point, so it should be enough to
5092 + // just call decode() which should also wait for the data to finish loading.
5093 + state.count++;
5094 + const ping = onUnsuspend.bind(state);
5095 + Promise.race([
5096 + // $FlowFixMe[prop-missing]
5097 + instance.decode(),
5098 + new Promise(resolve => setTimeout(resolve, SUSPENSEY_IMAGE_TIMEOUT)),
5099 + ]).then(ping, ping);
5100 + }
5101 }
5102
5103 export function suspendResource(
packages/react-native-renderer/src/ReactFiberConfigFabric.js
+25 -2
@@ -577,13 +577,36 @@ export function maySuspendCommit(type: Type, props: Props): boolean {
577 return false;
578 }
579
580 -export function preloadInstance(type: Type, props: Props): boolean {
580 +export function maySuspendCommitOnUpdate(
581 + type: Type,
582 + oldProps: Props,
583 + newProps: Props,
584 +): boolean {
585 + return false;
586 +}
587 +
588 +export function maySuspendCommitInSyncRender(
589 + type: Type,
590 + props: Props,
591 +): boolean {
592 + return false;
593 +}
594 +
595 +export function preloadInstance(
596 + instance: Instance,
597 + type: Type,
598 + props: Props,
599 +): boolean {
600 return true;
601 }
602
603 export function startSuspendingCommit(): void {}
604
586 -export function suspendInstance(type: Type, props: Props): void {}
605 +export function suspendInstance(
606 + instance: Instance,
607 + type: Type,
608 + props: Props,
609 +): void {}
610
611 export function suspendOnActiveViewTransition(container: Container): void {}
612
packages/react-native-renderer/src/ReactFiberConfigNative.js
+25 -2
@@ -735,14 +735,37 @@ export function maySuspendCommit(type: Type, props: Props): boolean {
735 return false;
736 }
737
738 -export function preloadInstance(type: Type, props: Props): boolean {
738 +export function maySuspendCommitOnUpdate(
739 + type: Type,
740 + oldProps: Props,
741 + newProps: Props,
742 +): boolean {
743 + return false;
744 +}
745 +
746 +export function maySuspendCommitInSyncRender(
747 + type: Type,
748 + props: Props,
749 +): boolean {
750 + return false;
751 +}
752 +
753 +export function preloadInstance(
754 + instance: Instance,
755 + type: Type,
756 + props: Props,
757 +): boolean {
758 // Return false to indicate it's already loaded
759 return true;
760 }
761
762 export function startSuspendingCommit(): void {}
763
745 -export function suspendInstance(type: Type, props: Props): void {}
764 +export function suspendInstance(
765 + instance: Instance,
766 + type: Type,
767 + props: Props,
768 +): void {}
769
770 export function suspendOnActiveViewTransition(container: Container): void {}
771
packages/react-noop-renderer/src/createReactNoop.js
+26 -2
@@ -320,7 +320,11 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
320 suspenseyCommitSubscription = null;
321 }
322
323 - function suspendInstance(type: string, props: Props): void {
323 + function suspendInstance(
324 + instance: Instance,
325 + type: string,
326 + props: Props,
327 + ): void {
328 const src = props.src;
329 if (type === 'suspensey-thing' && typeof src === 'string') {
330 // Attach a listener to the suspensey thing and create a subscription
@@ -624,13 +628,33 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
628 return type === 'suspensey-thing' && typeof props.src === 'string';
629 },
630
631 + maySuspendCommitOnUpdate(
632 + type: string,
633 + oldProps: Props,
634 + newProps: Props,
635 + ): boolean {
636 + // Asks whether it's possible for this combination of type and props
637 + // to ever need to suspend. This is different from asking whether it's
638 + // currently ready because even if it's ready now, it might get purged
639 + // from the cache later.
640 + return (
641 + type === 'suspensey-thing' &&
642 + typeof newProps.src === 'string' &&
643 + newProps.src !== oldProps.src
644 + );
645 + },
646 +
647 + maySuspendCommitInSyncRender(type: string, props: Props): boolean {
648 + return true;
649 + },
650 +
651 mayResourceSuspendCommit(resource: mixed): boolean {
652 throw new Error(
653 'Resources are not implemented for React Noop yet. This method should not be called',
654 );
655 },
656
633 - preloadInstance(type: string, props: Props): boolean {
657 + preloadInstance(instance: Instance, type: string, props: Props): boolean {
658 if (type !== 'suspensey-thing' || typeof props.src !== 'string') {
659 throw new Error('Attempted to preload unexpected instance: ' + type);
660 }
packages/react-reconciler/src/ReactFiberCommitWork.js
+40 -16
@@ -18,7 +18,10 @@ import type {
18 } from './ReactFiberConfig';
19 import type {Fiber, FiberRoot} from './ReactInternalTypes';
20 import type {Lanes} from './ReactFiberLane';
21 -import {includesOnlyViewTransitionEligibleLanes} from './ReactFiberLane';
21 +import {
22 + includesOnlySuspenseyCommitEligibleLanes,
23 + includesOnlyViewTransitionEligibleLanes,
24 +} from './ReactFiberLane';
25 import type {SuspenseState, RetryQueue} from './ReactFiberSuspenseComponent';
26 import type {UpdateQueue} from './ReactFiberClassUpdateQueue';
27 import type {FunctionComponentUpdateQueue} from './ReactFiberHooks';
@@ -160,6 +163,7 @@ import {
163 mountHoistable,
164 unmountHoistable,
165 prepareToCommitHoistables,
166 + maySuspendCommitInSyncRender,
167 suspendInstance,
168 suspendResource,
169 resetFormInstance,
@@ -4280,25 +4284,31 @@ export function commitPassiveUnmountEffects(finishedWork: Fiber): void {
4284 // ViewTransitions so that we know to also visit those to collect appearing
4285 // pairs.
4286 let suspenseyCommitFlag = ShouldSuspendCommit;
4283 -export function accumulateSuspenseyCommit(finishedWork: Fiber): void {
4287 +export function accumulateSuspenseyCommit(
4288 + finishedWork: Fiber,
4289 + committedLanes: Lanes,
4290 +): void {
4291 resetAppearingViewTransitions();
4285 - accumulateSuspenseyCommitOnFiber(finishedWork);
4292 + accumulateSuspenseyCommitOnFiber(finishedWork, committedLanes);
4293 }
4294
4288 -function recursivelyAccumulateSuspenseyCommit(parentFiber: Fiber): void {
4295 +function recursivelyAccumulateSuspenseyCommit(
4296 + parentFiber: Fiber,
4297 + committedLanes: Lanes,
4298 +): void {
4299 if (parentFiber.subtreeFlags & suspenseyCommitFlag) {
4300 let child = parentFiber.child;
4301 while (child !== null) {
4292 - accumulateSuspenseyCommitOnFiber(child);
4302 + accumulateSuspenseyCommitOnFiber(child, committedLanes);
4303 child = child.sibling;
4304 }
4305 }
4306 }
4307
4298 -function accumulateSuspenseyCommitOnFiber(fiber: Fiber) {
4308 +function accumulateSuspenseyCommitOnFiber(fiber: Fiber, committedLanes: Lanes) {
4309 switch (fiber.tag) {
4310 case HostHoistable: {
4301 - recursivelyAccumulateSuspenseyCommit(fiber);
4311 + recursivelyAccumulateSuspenseyCommit(fiber, committedLanes);
4312 if (fiber.flags & suspenseyCommitFlag) {
4313 if (fiber.memoizedState !== null) {
4314 suspendResource(
@@ -4308,19 +4318,33 @@ function accumulateSuspenseyCommitOnFiber(fiber: Fiber) {
4318 fiber.memoizedProps,
4319 );
4320 } else {
4321 + const instance = fiber.stateNode;
4322 const type = fiber.type;
4323 const props = fiber.memoizedProps;
4313 - suspendInstance(type, props);
4324 + // TODO: Allow sync lanes to suspend too with an opt-in.
4325 + if (
4326 + includesOnlySuspenseyCommitEligibleLanes(committedLanes) ||
4327 + maySuspendCommitInSyncRender(type, props)
4328 + ) {
4329 + suspendInstance(instance, type, props);
4330 + }
4331 }
4332 }
4333 break;
4334 }
4335 case HostComponent: {
4319 - recursivelyAccumulateSuspenseyCommit(fiber);
4336 + recursivelyAccumulateSuspenseyCommit(fiber, committedLanes);
4337 if (fiber.flags & suspenseyCommitFlag) {
4338 + const instance = fiber.stateNode;
4339 const type = fiber.type;
4340 const props = fiber.memoizedProps;
4323 - suspendInstance(type, props);
4341 + // TODO: Allow sync lanes to suspend too with an opt-in.
4342 + if (
4343 + includesOnlySuspenseyCommitEligibleLanes(committedLanes) ||
4344 + maySuspendCommitInSyncRender(type, props)
4345 + ) {
4346 + suspendInstance(instance, type, props);
4347 + }
4348 }
4349 break;
4350 }
@@ -4331,10 +4355,10 @@ function accumulateSuspenseyCommitOnFiber(fiber: Fiber) {
4355 const container: Container = fiber.stateNode.containerInfo;
4356 currentHoistableRoot = getHoistableRoot(container);
4357
4334 - recursivelyAccumulateSuspenseyCommit(fiber);
4358 + recursivelyAccumulateSuspenseyCommit(fiber, committedLanes);
4359 currentHoistableRoot = previousHoistableRoot;
4360 } else {
4337 - recursivelyAccumulateSuspenseyCommit(fiber);
4361 + recursivelyAccumulateSuspenseyCommit(fiber, committedLanes);
4362 }
4363 break;
4364 }
@@ -4352,10 +4376,10 @@ function accumulateSuspenseyCommitOnFiber(fiber: Fiber) {
4376 // instances, even if they're in the current tree.
4377 const prevFlags = suspenseyCommitFlag;
4378 suspenseyCommitFlag = MaySuspendCommit;
4355 - recursivelyAccumulateSuspenseyCommit(fiber);
4379 + recursivelyAccumulateSuspenseyCommit(fiber, committedLanes);
4380 suspenseyCommitFlag = prevFlags;
4381 } else {
4358 - recursivelyAccumulateSuspenseyCommit(fiber);
4382 + recursivelyAccumulateSuspenseyCommit(fiber, committedLanes);
4383 }
4384 }
4385 break;
@@ -4375,13 +4399,13 @@ function accumulateSuspenseyCommitOnFiber(fiber: Fiber) {
4399 trackAppearingViewTransition(name, state);
4400 }
4401 }
4378 - recursivelyAccumulateSuspenseyCommit(fiber);
4402 + recursivelyAccumulateSuspenseyCommit(fiber, committedLanes);
4403 break;
4404 }
4405 // Fallthrough
4406 }
4407 default: {
4384 - recursivelyAccumulateSuspenseyCommit(fiber);
4408 + recursivelyAccumulateSuspenseyCommit(fiber, committedLanes);
4409 }
4410 }
4411 }
packages/react-reconciler/src/ReactFiberCompleteWork.js
+33 -11
@@ -116,6 +116,8 @@ import {
116 preparePortalMount,
117 prepareScopeUpdate,
118 maySuspendCommit,
119 + maySuspendCommitOnUpdate,
120 + maySuspendCommitInSyncRender,
121 mayResourceSuspendCommit,
122 preloadInstance,
123 preloadResource,
@@ -167,6 +169,7 @@ import {
169 includesSomeLane,
170 mergeLanes,
171 claimNextRetryLane,
172 + includesOnlySuspenseyCommitEligibleLanes,
173 } from './ReactFiberLane';
174 import {resetChildFibers} from './ReactChildFiber';
175 import {createScopeInstance} from './ReactFiberScope';
@@ -547,10 +550,16 @@ function updateHostComponent(
550 function preloadInstanceAndSuspendIfNeeded(
551 workInProgress: Fiber,
552 type: Type,
550 - props: Props,
553 + oldProps: null | Props,
554 + newProps: Props,
555 renderLanes: Lanes,
556 ) {
553 - if (!maySuspendCommit(type, props)) {
557 + const maySuspend =
558 + oldProps === null
559 + ? maySuspendCommit(type, newProps)
560 + : maySuspendCommitOnUpdate(type, oldProps, newProps);
561 +
562 + if (!maySuspend) {
563 // If this flag was set previously, we can remove it. The flag
564 // represents whether this particular set of props might ever need to
565 // suspend. The safest thing to do is for maySuspendCommit to always
@@ -568,15 +577,25 @@ function preloadInstanceAndSuspendIfNeeded(
577 // loaded yet.
578 workInProgress.flags |= MaySuspendCommit;
579
571 - // preload the instance if necessary. Even if this is an urgent render there
572 - // could be benefits to preloading early.
573 - // @TODO we should probably do the preload in begin work
574 - const isReady = preloadInstance(type, props);
575 - if (!isReady) {
576 - if (shouldRemainOnPreviousScreen()) {
577 - workInProgress.flags |= ShouldSuspendCommit;
580 + if (
581 + includesOnlySuspenseyCommitEligibleLanes(renderLanes) ||
582 + maySuspendCommitInSyncRender(type, newProps)
583 + ) {
584 + // preload the instance if necessary. Even if this is an urgent render there
585 + // could be benefits to preloading early.
586 + // @TODO we should probably do the preload in begin work
587 + const isReady = preloadInstance(workInProgress.stateNode, type, newProps);
588 + if (!isReady) {
589 + if (shouldRemainOnPreviousScreen()) {
590 + workInProgress.flags |= ShouldSuspendCommit;
591 + } else {
592 + suspendCommit();
593 + }
594 } else {
579 - suspendCommit();
595 + // Even if we're ready we suspend the commit and check again in the pre-commit
596 + // phase if we need to suspend anyway. Such as if it's delayed on decoding or
597 + // if it was dropped from the cache while rendering due to pressure.
598 + workInProgress.flags |= ShouldSuspendCommit;
599 }
600 }
601 }
@@ -1104,6 +1123,7 @@ function completeWork(
1123 preloadInstanceAndSuspendIfNeeded(
1124 workInProgress,
1125 type,
1126 + null,
1127 newProps,
1128 renderLanes,
1129 );
@@ -1137,10 +1157,10 @@ function completeWork(
1157 return null;
1158 }
1159 } else {
1160 + const oldProps = current.memoizedProps;
1161 // This is an Instance
1162 // We may have props to update on the Hoistable instance.
1163 if (supportsMutation) {
1143 - const oldProps = current.memoizedProps;
1164 if (oldProps !== newProps) {
1165 markUpdate(workInProgress);
1166 }
@@ -1160,6 +1180,7 @@ function completeWork(
1180 preloadInstanceAndSuspendIfNeeded(
1181 workInProgress,
1182 type,
1183 + oldProps,
1184 newProps,
1185 renderLanes,
1186 );
@@ -1323,6 +1344,7 @@ function completeWork(
1344 preloadInstanceAndSuspendIfNeeded(
1345 workInProgress,
1346 workInProgress.type,
1347 + current === null ? null : current.memoizedProps,
1348 workInProgress.pendingProps,
1349 renderLanes,
1350 );
packages/react-reconciler/src/ReactFiberLane.js
+8
@@ -637,6 +637,14 @@ export function includesOnlyViewTransitionEligibleLanes(lanes: Lanes): boolean {
637 return (lanes & (TransitionLanes | RetryLanes | IdleLane)) === lanes;
638 }
639
640 +export function includesOnlySuspenseyCommitEligibleLanes(
641 + lanes: Lanes,
642 +): boolean {
643 + return (
644 + (lanes & (TransitionLanes | RetryLanes | IdleLane | GestureLane)) === lanes
645 + );
646 +}
647 +
648 export function includesBlockingLane(lanes: Lanes): boolean {
649 const SyncDefaultLanes =
650 InputContinuousHydrationLane |
packages/react-reconciler/src/ReactFiberPerformanceTrack.js
+2 -2
@@ -645,9 +645,9 @@ export function logSuspendedCommitPhase(
645 reusableLaneDevToolDetails.color = 'secondary-light';
646 reusableLaneOptions.start = startTime;
647 reusableLaneOptions.end = endTime;
648 - // TODO: Make this conditionally "Suspended on Images" or both when we add Suspensey Images.
648 + // TODO: Include the exact reason and URLs of what resources suspended.
649 // TODO: This might also be Suspended while waiting on a View Transition.
650 - performance.measure('Suspended on CSS', reusableLaneOptions);
650 + performance.measure('Suspended on CSS or Images', reusableLaneOptions);
651 }
652 }
653
packages/react-reconciler/src/ReactFiberWorkLoop.js
+2 -2
@@ -1467,7 +1467,7 @@ function commitRootWhenReady(
1467 // transaction, so it track state in its own module scope.
1468 // This will also track any newly added or appearing ViewTransition
1469 // components for the purposes of forming pairs.
1470 - accumulateSuspenseyCommit(finishedWork);
1470 + accumulateSuspenseyCommit(finishedWork, lanes);
1471 if (isViewTransitionEligible || isGestureTransition) {
1472 // If we're stopping gestures we don't have to wait for any pending
1473 // view transition. We'll stop it when we commit.
@@ -2638,7 +2638,7 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
2638 const props = hostFiber.pendingProps;
2639 const isReady = resource
2640 ? preloadResource(resource)
2641 - : preloadInstance(type, props);
2641 + : preloadInstance(hostFiber.stateNode, type, props);
2642 if (isReady) {
2643 // The data resolved. Resume the work loop as if nothing
2644 // suspended. Unlike when a user component suspends, we don't
packages/react-reconciler/src/__tests__/ReactFiberHostContext-test.internal.js
+8 -2
@@ -97,11 +97,17 @@ describe('ReactFiberHostContext', () => {
97 maySuspendCommit(type, props) {
98 return false;
99 },
100 - preloadInstance(type, props) {
100 + maySuspendCommitOnUpdate(type, oldProps, newProps) {
101 + return false;
102 + },
103 + maySuspendCommitInSyncRender(type, props) {
104 + return false;
105 + },
106 + preloadInstance(instance, type, props) {
107 return true;
108 },
109 startSuspendingCommit() {},
104 - suspendInstance(type, props) {},
110 + suspendInstance(instance, type, props) {},
111 suspendOnActiveViewTransition(container) {},
112 waitForCommitToBeReady() {
113 return null;
packages/react-reconciler/src/forks/ReactFiberConfig.custom.js
+3
@@ -88,6 +88,9 @@ export const shouldAttemptEagerTransition =
88 export const detachDeletedInstance = $$$config.detachDeletedInstance;
89 export const requestPostPaintCallback = $$$config.requestPostPaintCallback;
90 export const maySuspendCommit = $$$config.maySuspendCommit;
91 +export const maySuspendCommitOnUpdate = $$$config.maySuspendCommitOnUpdate;
92 +export const maySuspendCommitInSyncRender =
93 + $$$config.maySuspendCommitInSyncRender;
94 export const preloadInstance = $$$config.preloadInstance;
95 export const startSuspendingCommit = $$$config.startSuspendingCommit;
96 export const suspendInstance = $$$config.suspendInstance;
packages/react-test-renderer/src/ReactFiberConfigTestHost.js
+25 -2
@@ -537,14 +537,37 @@ export function maySuspendCommit(type: Type, props: Props): boolean {
537 return false;
538 }
539
540 -export function preloadInstance(type: Type, props: Props): boolean {
540 +export function maySuspendCommitOnUpdate(
541 + type: Type,
542 + oldProps: Props,
543 + newProps: Props,
544 +): boolean {
545 + return false;
546 +}
547 +
548 +export function maySuspendCommitInSyncRender(
549 + type: Type,
550 + props: Props,
551 +): boolean {
552 + return false;
553 +}
554 +
555 +export function preloadInstance(
556 + instance: Instance,
557 + type: Type,
558 + props: Props,
559 +): boolean {
560 // Return true to indicate it's already loaded
561 return true;
562 }
563
564 export function startSuspendingCommit(): void {}
565
547 -export function suspendInstance(type: Type, props: Props): void {}
566 +export function suspendInstance(
567 + instance: Instance,
568 + type: Type,
569 + props: Props,
570 +): void {}
571
572 export function suspendOnActiveViewTransition(container: Container): void {}
573
packages/shared/ReactFeatureFlags.js
+2
@@ -96,6 +96,8 @@ export const enableGestureTransition = __EXPERIMENTAL__;
96
97 export const enableScrollEndPolyfill = __EXPERIMENTAL__;
98
99 +export const enableSuspenseyImages = __EXPERIMENTAL__;
100 +
101 /**
102 * Switches the Fabric API from doing layout in commit work instead of complete work.
103 */
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -82,6 +82,7 @@ export const enableThrottledScheduling = false;
82 export const enableViewTransition = false;
83 export const enableGestureTransition = false;
84 export const enableScrollEndPolyfill = true;
85 +export const enableSuspenseyImages = false;
86 export const enableFragmentRefs = false;
87 export const ownerStackLimit = 1e4;
88
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -74,6 +74,7 @@ export const enableGestureTransition = false;
74 export const enableFastAddPropertiesInDiffing = false;
75 export const enableLazyPublicInstanceInFabric = false;
76 export const enableScrollEndPolyfill = true;
77 +export const enableSuspenseyImages = false;
78 export const ownerStackLimit = 1e4;
79
80 export const enableFragmentRefs = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -73,6 +73,7 @@ export const enableGestureTransition = false;
73 export const enableFastAddPropertiesInDiffing = true;
74 export const enableLazyPublicInstanceInFabric = false;
75 export const enableScrollEndPolyfill = true;
76 +export const enableSuspenseyImages = false;
77 export const ownerStackLimit = 1e4;
78
79 export const enableFragmentRefs = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js
+1
@@ -70,6 +70,7 @@ export const enableGestureTransition = false;
70 export const enableFastAddPropertiesInDiffing = false;
71 export const enableLazyPublicInstanceInFabric = false;
72 export const enableScrollEndPolyfill = true;
73 +export const enableSuspenseyImages = false;
74 export const enableFragmentRefs = false;
75 export const ownerStackLimit = 1e4;
76
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -84,6 +84,7 @@ export const enableGestureTransition = false;
84 export const enableFastAddPropertiesInDiffing = false;
85 export const enableLazyPublicInstanceInFabric = false;
86 export const enableScrollEndPolyfill = true;
87 +export const enableSuspenseyImages = false;
88
89 export const enableFragmentRefs = false;
90 export const ownerStackLimit = 1e4;
packages/shared/forks/ReactFeatureFlags.www.js
+2
@@ -113,6 +113,8 @@ export const enableLazyPublicInstanceInFabric = false;
113
114 export const enableGestureTransition = false;
115
116 +export const enableSuspenseyImages = false;
117 +
118 export const ownerStackLimit = 1e4;
119
120 // Flow magic to verify the exports of this file match the original version.