@@ -13,7 +13,7 @@ const SUSPENSE_PENDING_START_DATA = '$?';
1313const SUSPENSE_QUEUED_START_DATA = '$~' ;
1414const SUSPENSE_FALLBACK_START_DATA = '$!' ;
1515
16- const SUSPENSEY_FONT_TIMEOUT = 500 ;
16+ const SUSPENSEY_FONT_AND_IMAGE_TIMEOUT = 1000 ;
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