Skip to content

Commit 22b9291

Browse files
authored
[Fizz] Suspensey Images for View Transition Reveals (#33433)
Block the view transition on suspensey images Up to 500ms just like the client. We can't use `decode()` because a bug in Chrome where those are blocked on `startViewTransition` finishing we instead rely on sync decoding but also that the image is live when it's animating in and we assume it doesn't start visible. However, we can block the View Transition from starting on the `"load"` or `"error"` events. The nice thing about blocking inside `startViewTransition` is that we have already done the layout so we can only wait on images that are within the viewport at this point. We might want to do that in Fiber too. If many image doesn't have fixed size but need to load first, they can all end up in the viewport. We might consider only doing this for images that have a fixed size or only a max number that doesn't have a fixed size.
1 parent a3be682 commit 22b9291

File tree

3 files changed

+41
-10
lines changed

3 files changed

+41
-10
lines changed

packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4860,8 +4860,9 @@ export function writeCompletedSegmentInstruction(
48604860
const completeBoundaryScriptFunctionOnly = stringToPrecomputedChunk(
48614861
completeBoundaryFunction,
48624862
);
4863-
const completeBoundaryUpgradeToViewTransitionsInstruction =
4864-
stringToPrecomputedChunk(upgradeToViewTransitionsInstruction);
4863+
const completeBoundaryUpgradeToViewTransitionsInstruction = stringToChunk(
4864+
upgradeToViewTransitionsInstruction,
4865+
);
48654866
const completeBoundaryScript1Partial = stringToPrecomputedChunk('$RC("');
48664867

48674868
const completeBoundaryWithStylesScript1FullPartial = stringToPrecomputedChunk(

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/ReactDOMFizzInstructionSetShared.js

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const SUSPENSE_PENDING_START_DATA = '$?';
1313
const SUSPENSE_QUEUED_START_DATA = '$~';
1414
const SUSPENSE_FALLBACK_START_DATA = '$!';
1515

16-
const SUSPENSEY_FONT_TIMEOUT = 500;
16+
const SUSPENSEY_FONT_AND_IMAGE_TIMEOUT = 500;
1717

1818
// TODO: Symbols that are referenced outside this module use dynamic accessor
1919
// notation instead of dot notation to prevent Closure's advanced compilation
@@ -136,6 +136,7 @@ export function revealCompletedBoundariesWithViewTransitions(
136136
);
137137
}
138138
}
139+
const suspenseyImages = [];
139140
// Next we'll find the nodes that we're going to animate and apply names to them..
140141
for (let i = 0; i < batch.length; i += 2) {
141142
const suspenseIdNode = batch[i];
@@ -248,21 +249,50 @@ export function revealCompletedBoundariesWithViewTransitions(
248249
ancestorElement.nodeType === ELEMENT_NODE &&
249250
ancestorElement.getAttribute('vt-update') !== 'none'
250251
);
252+
253+
// Find the appearing Suspensey Images inside the new content.
254+
const appearingImages = contentNode.querySelectorAll(
255+
'img[src]:not([loading="lazy"])',
256+
);
257+
// TODO: Consider marking shouldStartViewTransition if we found any images.
258+
// But only once we can disable the root animation for that case.
259+
suspenseyImages.push.apply(suspenseyImages, appearingImages);
251260
}
252261
if (shouldStartViewTransition) {
253262
const transition = (document['__reactViewTransition'] = document[
254263
'startViewTransition'
255264
]({
256265
update: () => {
257-
revealBoundaries(
258-
batch,
259-
// Force layout to trigger font loading, we pass the actual value to trick minifiers.
266+
revealBoundaries(batch);
267+
const blockingPromises = [
268+
// Force layout to trigger font loading, we stash the actual value to trick minifiers.
260269
document.documentElement.clientHeight,
261-
);
262-
return Promise.race([
263270
// Block on fonts finishing loading before revealing these boundaries.
264271
document.fonts.ready,
265-
new Promise(resolve => setTimeout(resolve, SUSPENSEY_FONT_TIMEOUT)),
272+
];
273+
for (let i = 0; i < suspenseyImages.length; i++) {
274+
const suspenseyImage = suspenseyImages[i];
275+
if (!suspenseyImage.complete) {
276+
const rect = suspenseyImage.getBoundingClientRect();
277+
const inViewport =
278+
rect.bottom > 0 &&
279+
rect.right > 0 &&
280+
rect.top < window.innerHeight &&
281+
rect.left < window.innerWidth;
282+
if (inViewport) {
283+
const loadingImage = new Promise(resolve => {
284+
suspenseyImage.addEventListener('load', resolve);
285+
suspenseyImage.addEventListener('error', resolve);
286+
});
287+
blockingPromises.push(loadingImage);
288+
}
289+
}
290+
}
291+
return Promise.race([
292+
Promise.all(blockingPromises),
293+
new Promise(resolve =>
294+
setTimeout(resolve, SUSPENSEY_FONT_AND_IMAGE_TIMEOUT),
295+
),
266296
]);
267297
},
268298
types: [], // TODO: Add a hard coded type for Suspense reveals.

0 commit comments

Comments
 (0)