@@ -367,6 +367,7 @@ export function getInternalReactConstants(version: string): {
367367 ReactPriorityLevels : ReactPriorityLevelsType ,
368368 ReactTypeOfWork : WorkTagMap ,
369369 StrictModeBits : number ,
370+ SuspenseyImagesMode : number ,
370371} {
371372 // **********************************************************
372373 // The section below is copied from files in React repo.
@@ -407,6 +408,8 @@ export function getInternalReactConstants(version: string): {
407408 StrictModeBits = 0b10 ;
408409 }
409410
411+ const SuspenseyImagesMode = 0b0100000 ;
412+
410413 let ReactTypeOfWork : WorkTagMap = ( ( null : any ) : WorkTagMap ) ;
411414
412415 // **********************************************************
@@ -820,6 +823,7 @@ export function getInternalReactConstants(version: string): {
820823 ReactPriorityLevels,
821824 ReactTypeOfWork,
822825 StrictModeBits,
826+ SuspenseyImagesMode,
823827 };
824828}
825829
@@ -988,6 +992,7 @@ export function attach(
988992 ReactPriorityLevels,
989993 ReactTypeOfWork,
990994 StrictModeBits,
995+ SuspenseyImagesMode,
991996 } = getInternalReactConstants(version);
992997 const {
993998 ActivityComponent,
@@ -3345,6 +3350,114 @@ export function attach(
33453350 insertSuspendedBy(asyncInfo);
33463351 }
33473352
3353+ function trackDebugInfoFromHostComponent(
3354+ devtoolsInstance: DevToolsInstance,
3355+ fiber: Fiber,
3356+ ): void {
3357+ if (fiber.tag !== HostComponent) {
3358+ return;
3359+ }
3360+ if ((fiber.mode & SuspenseyImagesMode) === 0) {
3361+ // In any released version, Suspensey Images are only enabled inside a ViewTransition
3362+ // subtree, which is enabled by the SuspenseyImagesMode.
3363+ // TODO: If we ever enable the enableSuspenseyImages flag then it would be enabled for
3364+ // all images and we'd need some other check for if the version of React has that enabled.
3365+ return;
3366+ }
3367+
3368+ const type = fiber.type;
3369+ const props: {
3370+ src?: string,
3371+ onLoad?: (event: any) => void,
3372+ loading?: 'eager' | 'lazy',
3373+ ...
3374+ } = fiber.memoizedProps;
3375+
3376+ const maySuspendCommit =
3377+ type === 'img' &&
3378+ props.src != null &&
3379+ props.src !== '' &&
3380+ props.onLoad == null &&
3381+ props.loading !== 'lazy';
3382+
3383+ // Note: We don't track "maySuspendCommitOnUpdate" separately because it doesn't matter if
3384+ // it didn't suspend this particular update if it would've suspended if it mounted in this
3385+ // state, since we're tracking the dependencies inside the current state.
3386+
3387+ if (!maySuspendCommit) {
3388+ return;
3389+ }
3390+
3391+ const instance = fiber.stateNode;
3392+ if (instance == null) {
3393+ // Should never happen.
3394+ return;
3395+ }
3396+
3397+ // Unlike props.src, currentSrc will be fully qualified which we need for comparison below.
3398+ // Unlike instance.src it will be resolved into the media queries currently matching which is
3399+ // the state we're inspecting.
3400+ const src = instance.currentSrc;
3401+ if (typeof src !== 'string' || src === '') {
3402+ return;
3403+ }
3404+ let start = -1;
3405+ let end = -1;
3406+ let fileSize = 0;
3407+ // $FlowFixMe[method-unbinding]
3408+ if (typeof performance.getEntriesByType === 'function') {
3409+ // We may be able to collect the start and end time of this resource from Performance Observer.
3410+ const resourceEntries = performance.getEntriesByType('resource');
3411+ for (let i = 0; i < resourceEntries.length; i++) {
3412+ const resourceEntry = resourceEntries[i];
3413+ if (resourceEntry.name === src) {
3414+ start = resourceEntry.startTime;
3415+ end = start + resourceEntry.duration;
3416+ // $FlowFixMe[prop-missing]
3417+ fileSize = (resourceEntry.encodedBodySize: any) || 0;
3418+ }
3419+ }
3420+ }
3421+ // A representation of the image data itself.
3422+ // TODO: We could render a little preview in the front end from the resource API.
3423+ const value: {
3424+ currentSrc: string,
3425+ naturalWidth?: number,
3426+ naturalHeight?: number,
3427+ fileSize?: number,
3428+ } = {
3429+ currentSrc: src,
3430+ };
3431+ if (instance.naturalWidth > 0 && instance.naturalHeight > 0) {
3432+ // The intrinsic size of the file value itself, if it's loaded
3433+ value.naturalWidth = instance.naturalWidth;
3434+ value.naturalHeight = instance.naturalHeight;
3435+ }
3436+ if (fileSize > 0) {
3437+ // Cross-origin images won't have a file size that we can access.
3438+ value.fileSize = fileSize;
3439+ }
3440+ const promise = Promise.resolve(value);
3441+ (promise: any).status = 'fulfilled';
3442+ (promise: any).value = value;
3443+ const ioInfo: ReactIOInfo = {
3444+ name: 'img',
3445+ start,
3446+ end,
3447+ value: promise,
3448+ // $FlowFixMe: This field doesn't usually take a Fiber but we're only using inside this file.
3449+ owner: fiber, // Allow linking to the <link> if it's not filtered.
3450+ };
3451+ const asyncInfo: ReactAsyncInfo = {
3452+ awaited: ioInfo,
3453+ // $FlowFixMe: This field doesn't usually take a Fiber but we're only using inside this file.
3454+ owner: fiber._debugOwner == null ? null : fiber._debugOwner,
3455+ debugStack: fiber._debugStack == null ? null : fiber._debugStack,
3456+ debugTask: fiber._debugTask == null ? null : fiber._debugTask,
3457+ };
3458+ insertSuspendedBy(asyncInfo);
3459+ }
3460+
33483461 function mountVirtualChildrenRecursively(
33493462 firstChild: Fiber,
33503463 lastChild: null | Fiber, // non-inclusive
@@ -3619,6 +3732,7 @@ export function attach(
36193732 throw new Error('Did not expect a host hoistable to be the root');
36203733 }
36213734 aquireHostInstance(nearestInstance, fiber.stateNode);
3735+ trackDebugInfoFromHostComponent(nearestInstance, fiber);
36223736 }
36233737
36243738 if (fiber.tag === OffscreenComponent && fiber.memoizedState !== null) {
@@ -4447,20 +4561,22 @@ export function attach(
44474561 aquireHostResource(nearestInstance, nextFiber.memoizedState);
44484562 trackDebugInfoFromHostResource(nearestInstance, nextFiber);
44494563 } else if (
4450- (nextFiber.tag === HostComponent ||
4451- nextFiber.tag === HostText ||
4452- nextFiber.tag === HostSingleton) &&
4453- prevFiber.stateNode !== nextFiber.stateNode
4564+ nextFiber.tag === HostComponent ||
4565+ nextFiber.tag === HostText ||
4566+ nextFiber.tag === HostSingleton
44544567 ) {
4455- // In persistent mode, it's possible for the stateNode to update with
4456- // a new clone. In that case we need to release the old one and aquire
4457- // new one instead.
44584568 const nearestInstance = reconcilingParent;
44594569 if (nearestInstance === null) {
44604570 throw new Error('Did not expect a host hoistable to be the root');
44614571 }
4462- releaseHostInstance(nearestInstance, prevFiber.stateNode);
4463- aquireHostInstance(nearestInstance, nextFiber.stateNode);
4572+ if (prevFiber.stateNode !== nextFiber.stateNode) {
4573+ // In persistent mode, it's possible for the stateNode to update with
4574+ // a new clone. In that case we need to release the old one and aquire
4575+ // new one instead.
4576+ releaseHostInstance(nearestInstance, prevFiber.stateNode);
4577+ aquireHostInstance(nearestInstance, nextFiber.stateNode);
4578+ }
4579+ trackDebugInfoFromHostComponent(nearestInstance, nextFiber);
44644580 }
44654581
44664582 let updateFlags = NoUpdate;
0 commit comments