@@ -2010,7 +2010,8 @@ function cancelAllViewTransitionAnimations(scope: Element) {
20102010// an issue when it's a new load and slow, yet long enough that you have a chance to load
20112011// it. Otherwise we wait for no reason. The assumption here is that you likely have
20122012// either cached the font or preloaded it earlier.
2013- const SUSPENSEY_FONT_TIMEOUT = 500 ;
2013+ // This timeout is also used for Suspensey Images when they're blocking a View Transition.
2014+ const SUSPENSEY_FONT_AND_IMAGE_TIMEOUT = 500 ;
20142015
20152016function customizeViewTransitionError (
20162017 error : Object ,
@@ -2080,6 +2081,13 @@ function forceLayout(ownerDocument: Document) {
20802081 return ( ownerDocument . documentElement : any ) . clientHeight ;
20812082}
20822083
2084+ function waitForImageToLoad ( this : HTMLImageElement , resolve : ( ) = > void ) {
2085+ // TODO: Use decode() instead of the load event here once the fix in
2086+ // https://issues.chromium.org/issues/420748301 has propagated fully.
2087+ this. addEventListener ( 'load' , resolve ) ;
2088+ this . addEventListener ( 'error' , resolve ) ;
2089+ }
2090+
20832091export function startViewTransition (
20842092 suspendedState : null | SuspendedState ,
20852093 rootContainer : Container ,
@@ -2108,6 +2116,7 @@ export function startViewTransition(
21082116 // $FlowFixMe[prop-missing]
21092117 const previousFontLoadingStatus = ownerDocument . fonts . status ;
21102118 mutationCallback ( ) ;
2119+ const blockingPromises : Array < Promise < any > > = [ ] ;
21112120 if ( previousFontLoadingStatus === 'loaded' ) {
21122121 // Force layout calculation to trigger font loading.
21132122 forceLayout ( ownerDocument ) ;
@@ -2119,19 +2128,51 @@ export function startViewTransition(
21192128 // This avoids waiting for potentially unrelated fonts that were already loading before.
21202129 // Either in an earlier transition or as part of a sync optimistic state. This doesn't
21212130 // include preloads that happened earlier.
2122- const fontsReady = Promise . race ( [
2123- // $FlowFixMe[prop-missing]
2124- ownerDocument . fonts . ready ,
2125- new Promise ( resolve =>
2126- setTimeout ( resolve , SUSPENSEY_FONT_TIMEOUT ) ,
2127- ) ,
2128- ] ) . then ( layoutCallback , layoutCallback ) ;
2129- const allReady = pendingNavigation
2130- ? Promise . allSettled ( [ pendingNavigation . finished , fontsReady ] )
2131- : fontsReady ;
2132- return allReady . then ( afterMutationCallback , afterMutationCallback ) ;
2131+ blockingPromises . push ( ownerDocument . fonts . ready ) ;
21332132 }
21342133 }
2134+ if ( suspendedState !== null ) {
2135+ // Suspend on any images that still haven't loaded and are in the viewport.
2136+ const suspenseyImages = suspendedState . suspenseyImages ;
2137+ const blockingIndexSnapshot = blockingPromises . length ;
2138+ let imgBytes = 0 ;
2139+ for ( let i = 0 ; i < suspenseyImages . length ; i ++ ) {
2140+ const suspenseyImage = suspenseyImages [ i ] ;
2141+ if ( ! suspenseyImage . complete ) {
2142+ const rect = suspenseyImage . getBoundingClientRect ( ) ;
2143+ const inViewport =
2144+ rect . bottom > 0 &&
2145+ rect . right > 0 &&
2146+ rect . top < ownerWindow . innerHeight &&
2147+ rect . left < ownerWindow . innerWidth ;
2148+ if ( inViewport ) {
2149+ imgBytes += estimateImageBytes ( suspenseyImage ) ;
2150+ if ( imgBytes > estimatedBytesWithinLimit ) {
2151+ // We don't think we'll be able to download all the images within
2152+ // the timeout. Give up. Rewind to only block on fonts, if any.
2153+ blockingPromises . length = blockingIndexSnapshot ;
2154+ break ;
2155+ }
2156+ const loadingImage = new Promise (
2157+ waitForImageToLoad . bind ( suspenseyImage ) ,
2158+ ) ;
2159+ blockingPromises . push ( loadingImage ) ;
2160+ }
2161+ }
2162+ }
2163+ }
2164+ if ( blockingPromises . length > 0 ) {
2165+ const blockingReady = Promise . race ( [
2166+ Promise . all ( blockingPromises ) ,
2167+ new Promise ( resolve =>
2168+ setTimeout ( resolve , SUSPENSEY_FONT_AND_IMAGE_TIMEOUT ) ,
2169+ ) ,
2170+ ] ) . then ( layoutCallback , layoutCallback ) ;
2171+ const allReady = pendingNavigation
2172+ ? Promise . allSettled ( [ pendingNavigation . finished , blockingReady ] )
2173+ : blockingReady ;
2174+ return allReady . then ( afterMutationCallback , afterMutationCallback ) ;
2175+ }
21352176 layoutCallback ( ) ;
21362177 if ( pendingNavigation ) {
21372178 return pendingNavigation . finished . then (
@@ -5909,8 +5950,9 @@ export function preloadResource(resource: Resource): boolean {
59095950export opaque type SuspendedState = {
59105951 stylesheets : null | Map < StylesheetResource , HoistableRoot > ,
59115952 count : number , // suspensey css and active view transitions
5912- imgCount : number , // suspensey images
5953+ imgCount : number , // suspensey images pending to load
59135954 imgBytes : number , // number of bytes we estimate needing to download
5955+ suspenseyImages : Array < HTMLImageElement > , // instances of suspensey images (whether loaded or not)
59145956 waitingForImages : boolean , // false when we're no longer blocking on images
59155957 unsuspend : null | ( ( ) => void ) ,
59165958} ;
@@ -5921,6 +5963,7 @@ export function startSuspendingCommit(): SuspendedState {
59215963 count : 0 ,
59225964 imgCount : 0 ,
59235965 imgBytes : 0 ,
5966+ suspenseyImages : [ ] ,
59245967 waitingForImages : true ,
59255968 // We use a noop function when we begin suspending because if possible we want the
59265969 // waitfor step to finish synchronously. If it doesn't we'll return a function to
@@ -5930,6 +5973,16 @@ export function startSuspendingCommit(): SuspendedState {
59305973 } ;
59315974}
59325975
5976+ function estimateImageBytes ( instance : HTMLImageElement ) : number {
5977+ const width : number = instance . width || 100 ;
5978+ const height : number = instance . height || 100 ;
5979+ const pixelRatio : number =
5980+ typeof devicePixelRatio === 'number' ? devicePixelRatio : 1 ;
5981+ const pixelsToDownload = width * height * pixelRatio ;
5982+ const AVERAGE_BYTE_PER_PIXEL = 0.25 ;
5983+ return pixelsToDownload * AVERAGE_BYTE_PER_PIXEL ;
5984+ }
5985+
59335986export function suspendInstance (
59345987 state : SuspendedState ,
59355988 instance : Instance ,
@@ -5941,8 +5994,7 @@ export function suspendInstance(
59415994 }
59425995 if (
59435996 // $FlowFixMe[prop-missing]
5944- typeof instance . decode === 'function' &&
5945- typeof setTimeout === 'function'
5997+ typeof instance . decode === 'function'
59465998 ) {
59475999 // If this browser supports decode() API, we use it to suspend waiting on the image.
59486000 // The loading should have already started at this point, so it should be enough to
@@ -5952,13 +6004,8 @@ export function suspendInstance(
59526004 // specified in the props. This is best practice to know ahead of time but if it's
59536005 // unspecified we'll fallback to a guess of 100x100 pixels.
59546006 if ( ! ( instance : any ) . complete ) {
5955- const width : number = ( instance : any ) . width || 100 ;
5956- const height : number = ( instance : any ) . height || 100 ;
5957- const pixelRatio : number =
5958- typeof devicePixelRatio === 'number' ? devicePixelRatio : 1 ;
5959- const pixelsToDownload = width * height * pixelRatio ;
5960- const AVERAGE_BYTE_PER_PIXEL = 0.25 ;
5961- state . imgBytes += pixelsToDownload * AVERAGE_BYTE_PER_PIXEL ;
6007+ state . imgBytes += estimateImageBytes ( ( instance : any ) ) ;
6008+ state . suspenseyImages . push ( ( instance : any ) ) ;
59626009 }
59636010 const ping = onUnsuspendImg . bind ( state ) ;
59646011 // $FlowFixMe[prop-missing]
0 commit comments