From 22b929156c325eaf52c375f0c62801831951814a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Fri, 6 Jun 2025 10:14:13 -0400 Subject: [PATCH] [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. --- .../src/server/ReactFizzConfigDOM.js | 5 ++- ...tDOMFizzInstructionSetInlineCodeStrings.js | 2 +- .../ReactDOMFizzInstructionSetShared.js | 44 ++++++++++++++++--- 3 files changed, 41 insertions(+), 10 deletions(-) diff --git a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js index 795a406690..44d3f20a61 100644 --- a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js +++ b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js @@ -4860,8 +4860,9 @@ export function writeCompletedSegmentInstruction( const completeBoundaryScriptFunctionOnly = stringToPrecomputedChunk( completeBoundaryFunction, ); -const completeBoundaryUpgradeToViewTransitionsInstruction = - stringToPrecomputedChunk(upgradeToViewTransitionsInstruction); +const completeBoundaryUpgradeToViewTransitionsInstruction = stringToChunk( + upgradeToViewTransitionsInstruction, +); const completeBoundaryScript1Partial = stringToPrecomputedChunk('$RC("'); const completeBoundaryWithStylesScript1FullPartial = stringToPrecomputedChunk( diff --git a/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js index eacd86aa06..72a5aba4fb 100644 --- a/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js +++ b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js @@ -8,7 +8,7 @@ export const clientRenderBoundary = export const completeBoundary = '$RB=[];$RV=function(c){$RT=performance.now();for(var a=0;a { - revealBoundaries( - batch, - // Force layout to trigger font loading, we pass the actual value to trick minifiers. + revealBoundaries(batch); + const blockingPromises = [ + // Force layout to trigger font loading, we stash the actual value to trick minifiers. document.documentElement.clientHeight, - ); - return Promise.race([ // Block on fonts finishing loading before revealing these boundaries. document.fonts.ready, - new Promise(resolve => setTimeout(resolve, SUSPENSEY_FONT_TIMEOUT)), + ]; + for (let i = 0; i < suspenseyImages.length; i++) { + const suspenseyImage = suspenseyImages[i]; + if (!suspenseyImage.complete) { + const rect = suspenseyImage.getBoundingClientRect(); + const inViewport = + rect.bottom > 0 && + rect.right > 0 && + rect.top < window.innerHeight && + rect.left < window.innerWidth; + if (inViewport) { + const loadingImage = new Promise(resolve => { + suspenseyImage.addEventListener('load', resolve); + suspenseyImage.addEventListener('error', resolve); + }); + blockingPromises.push(loadingImage); + } + } + } + return Promise.race([ + Promise.all(blockingPromises), + new Promise(resolve => + setTimeout(resolve, SUSPENSEY_FONT_AND_IMAGE_TIMEOUT), + ), ]); }, types: [], // TODO: Add a hard coded type for Suspense reveals.