@@ -1138,125 +1138,172 @@ function renderSuspenseBoundary(
11381138 // no parent segment so there's nothing to wait on.
11391139 contentRootSegment.parentFlushed = true;
11401140
1141- // Currently this is running synchronously. We could instead schedule this to pingedTasks.
1142- // I suspect that there might be some efficiency benefits from not creating the suspended task
1143- // and instead just using the stack if possible.
1144- // TODO: Call this directly instead of messing with saving and restoring contexts.
1141+ if (request.trackedPostpones !== null) {
1142+ // This is a prerender. In this mode we want to render the fallback synchronously and schedule
1143+ // the content to render later. This is the opposite of what we do during a normal render
1144+ // where we try to skip rendering the fallback if the content itself can render synchronously
1145+ const trackedPostpones = request . trackedPostpones ;
11451146
1146- // We can reuse the current context and task to render the content immediately without
1147- // context switching. We just need to temporarily switch which boundary and which segment
1148- // we're writing to. If something suspends, it'll spawn new suspended task with that context.
1149- task.blockedBoundary = newBoundary;
1150- task.hoistableState = newBoundary.contentState;
1151- task.blockedSegment = contentRootSegment;
1152- task.keyPath = keyPath;
1147+ const fallbackKeyPath = [ keyPath [ 0 ] , 'Suspense Fallback' , keyPath [ 2 ] ] ;
1148+ const fallbackReplayNode : ReplayNode = [
1149+ fallbackKeyPath [ 1 ] ,
1150+ fallbackKeyPath [ 2 ] ,
1151+ ( [ ] : Array < ReplayNode > ) ,
1152+ null ,
1153+ ] ;
1154+ trackedPostpones . workingMap . set ( fallbackKeyPath , fallbackReplayNode ) ;
1155+ // We are rendering the fallback before the boundary content so we keep track of
1156+ // the fallback replay node until we determine if the primary content suspends
1157+ newBoundary . trackedFallbackNode = fallbackReplayNode ;
11531158
1154- try {
1155- // We use the safe form because we don't handle suspending here. Only error handling.
1156- renderNode ( request , task , content , - 1 ) ;
1157- pushSegmentFinale (
1158- contentRootSegment . chunks ,
1159- request . renderState ,
1160- contentRootSegment . lastPushedText ,
1161- contentRootSegment . textEmbedded ,
1162- ) ;
1163- contentRootSegment . status = COMPLETED ;
1164- queueCompletedSegment ( newBoundary , contentRootSegment ) ;
1165- if ( newBoundary . pendingTasks === 0 && newBoundary . status === PENDING ) {
1166- // This must have been the last segment we were waiting on. This boundary is now complete.
1167- // Therefore we won't need the fallback. We early return so that we don't have to create
1168- // the fallback.
1169- newBoundary. status = COMPLETED ;
1170- return ;
1159+ task . blockedSegment = boundarySegment ;
1160+ task . keyPath = fallbackKeyPath ;
1161+ try {
1162+ renderNode ( request , task , fallback , - 1 ) ;
1163+ pushSegmentFinale (
1164+ boundarySegment . chunks ,
1165+ request . renderState ,
1166+ boundarySegment . lastPushedText ,
1167+ boundarySegment . textEmbedded ,
1168+ ) ;
1169+ boundarySegment . status = COMPLETED ;
1170+ } finally {
1171+ task . blockedSegment = parentSegment ;
1172+ task . keyPath = prevKeyPath ;
11711173 }
1172- } catch ( error : mixed ) {
1173- contentRootSegment . status = ERRORED ;
1174- newBoundary . status = CLIENT_RENDERED ;
1175- const thrownInfo = getThrownInfo ( task . componentStack ) ;
1176- let errorDigest ;
1177- if (
1178- enablePostpone &&
1179- typeof error === 'object' &&
1180- error !== null &&
1181- error . $$typeof === REACT_POSTPONE_TYPE
1182- ) {
1183- const postponeInstance : Postpone = ( error : any ) ;
1184- logPostpone (
1185- request ,
1186- postponeInstance . message ,
1187- thrownInfo ,
1188- __DEV__ && enableOwnerStacks ? task . debugTask : null ,
1174+
1175+ // We create a suspended task for the primary content because we want to allow
1176+ // sibling fallbacks to be rendered first.
1177+ const suspendedPrimaryTask = createRenderTask (
1178+ request ,
1179+ null ,
1180+ content ,
1181+ - 1 ,
1182+ newBoundary ,
1183+ contentRootSegment ,
1184+ newBoundary . contentState ,
1185+ task . abortSet ,
1186+ keyPath ,
1187+ task . formatContext ,
1188+ task . context ,
1189+ task . treeContext ,
1190+ task . componentStack ,
1191+ task . isFallback ,
1192+ ! disableLegacyContext ? task . legacyContext : emptyContextObject ,
1193+ __DEV__ && enableOwnerStacks ? task . debugTask : null ,
1194+ ) ;
1195+ pushComponentStack ( suspendedPrimaryTask ) ;
1196+ request . pingedTasks . push ( suspendedPrimaryTask ) ;
1197+ } else {
1198+ // This is a normal render. We will attempt to synchronously render the boundary content
1199+ // If it is successful we will elide the fallback task but if it suspends or errors we schedule
1200+ // the fallback to render. Unlike with prerenders we attempt to deprioritize the fallback render
1201+
1202+ // Currently this is running synchronously. We could instead schedule this to pingedTasks.
1203+ // I suspect that there might be some efficiency benefits from not creating the suspended task
1204+ // and instead just using the stack if possible.
1205+ // TODO: Call this directly instead of messing with saving and restoring contexts.
1206+
1207+ // We can reuse the current context and task to render the content immediately without
1208+ // context switching. We just need to temporarily switch which boundary and which segment
1209+ // we're writing to. If something suspends, it'll spawn new suspended task with that context.
1210+ task . blockedBoundary = newBoundary ;
1211+ task . hoistableState = newBoundary . contentState ;
1212+ task . blockedSegment = contentRootSegment ;
1213+ task . keyPath = keyPath ;
1214+
1215+ try {
1216+ // We use the safe form because we don't handle suspending here. Only error handling.
1217+ renderNode ( request , task , content , - 1 ) ;
1218+ pushSegmentFinale (
1219+ contentRootSegment . chunks ,
1220+ request . renderState ,
1221+ contentRootSegment . lastPushedText ,
1222+ contentRootSegment . textEmbedded ,
11891223 ) ;
1190- // TODO: Figure out a better signal than a magic digest value.
1191- errorDigest = 'POSTPONE' ;
1192- } else {
1193- errorDigest = logRecoverableError (
1194- request ,
1224+ contentRootSegment . status = COMPLETED ;
1225+ queueCompletedSegment ( newBoundary , contentRootSegment ) ;
1226+ if ( newBoundary . pendingTasks === 0 && newBoundary . status === PENDING ) {
1227+ // This must have been the last segment we were waiting on. This boundary is now complete.
1228+ // Therefore we won't need the fallback. We early return so that we don't have to create
1229+ // the fallback.
1230+ newBoundary . status = COMPLETED ;
1231+ return ;
1232+ }
1233+ } catch ( error : mixed ) {
1234+ contentRootSegment . status = ERRORED ;
1235+ newBoundary . status = CLIENT_RENDERED ;
1236+ const thrownInfo = getThrownInfo ( task . componentStack ) ;
1237+ let errorDigest ;
1238+ if (
1239+ enablePostpone &&
1240+ typeof error === 'object' &&
1241+ error !== null &&
1242+ error . $$typeof === REACT_POSTPONE_TYPE
1243+ ) {
1244+ const postponeInstance : Postpone = ( error : any ) ;
1245+ logPostpone (
1246+ request ,
1247+ postponeInstance . message ,
1248+ thrownInfo ,
1249+ __DEV__ && enableOwnerStacks ? task . debugTask : null ,
1250+ ) ;
1251+ // TODO: Figure out a better signal than a magic digest value.
1252+ errorDigest = 'POSTPONE ';
1253+ } else {
1254+ errorDigest = logRecoverableError (
1255+ request ,
1256+ error ,
1257+ thrownInfo ,
1258+ __DEV__ && enableOwnerStacks ? task . debugTask : null ,
1259+ ) ;
1260+ }
1261+ encodeErrorForBoundary (
1262+ newBoundary ,
1263+ errorDigest ,
11951264 error ,
11961265 thrownInfo ,
1197- __DEV__ && enableOwnerStacks ? task . debugTask : null ,
1266+ false ,
11981267 ) ;
1199- }
1200- encodeErrorForBoundary ( newBoundary , errorDigest , error , thrownInfo , false ) ;
12011268
1202- untrackBoundary ( request , newBoundary ) ;
1269+ untrackBoundary ( request , newBoundary ) ;
12031270
1204- // We don't need to decrement any task numbers because we didn't spawn any new task.
1205- // We don't need to schedule any task because we know the parent has written yet.
1206- // We do need to fallthrough to create the fallback though.
1207- } finally {
1208- task . blockedBoundary = parentBoundary ;
1209- task . hoistableState = parentHoistableState ;
1210- task . blockedSegment = parentSegment ;
1211- task . keyPath = prevKeyPath ;
1212- }
1271+ // We don't need to decrement any task numbers because we didn't spawn any new task.
1272+ // We don't need to schedule any task because we know the parent has written yet.
1273+ // We do need to fallthrough to create the fallback though.
1274+ } finally {
1275+ task . blockedBoundary = parentBoundary ;
1276+ task . hoistableState = parentHoistableState ;
1277+ task . blockedSegment = parentSegment ;
1278+ task . keyPath = prevKeyPath ;
1279+ }
12131280
1214- const fallbackKeyPath = [ keyPath [ 0 ] , 'Suspense Fallback' , keyPath [ 2 ] ] ;
1215- const trackedPostpones = request . trackedPostpones ;
1216- if ( trackedPostpones !== null ) {
1217- // We create a detached replay node to track any postpones inside the fallback.
1218- const fallbackReplayNode : ReplayNode = [
1219- fallbackKeyPath [ 1 ] ,
1220- fallbackKeyPath [ 2 ] ,
1221- ( [ ] : Array < ReplayNode > ) ,
1281+ const fallbackKeyPath = [ keyPath [ 0 ] , 'Suspense Fallback' , keyPath [ 2 ] ] ;
1282+ // We create suspended task for the fallback because we don't want to actually work
1283+ // on it yet in case we finish the main content, so we queue for later.
1284+ const suspendedFallbackTask = createRenderTask (
1285+ request ,
12221286 null ,
1223- ] ;
1224- trackedPostpones . workingMap . set ( fallbackKeyPath , fallbackReplayNode ) ;
1225- if ( newBoundary . status === POSTPONED ) {
1226- // This must exist now.
1227- const boundaryReplayNode : ReplaySuspenseBoundary =
1228- ( trackedPostpones . workingMap . get ( keyPath ) : any ) ;
1229- boundaryReplayNode [ 4 ] = fallbackReplayNode ;
1230- } else {
1231- // We might not inject it into the postponed tree, unless the content actually
1232- // postpones too. We need to keep track of it until that happpens.
1233- newBoundary . trackedFallbackNode = fallbackReplayNode ;
1234- }
1287+ fallback ,
1288+ - 1 ,
1289+ parentBoundary ,
1290+ boundarySegment ,
1291+ newBoundary . fallbackState ,
1292+ fallbackAbortSet ,
1293+ fallbackKeyPath ,
1294+ task . formatContext ,
1295+ task . context ,
1296+ task . treeContext ,
1297+ task . componentStack ,
1298+ true ,
1299+ ! disableLegacyContext ? task . legacyContext : emptyContextObject ,
1300+ __DEV__ && enableOwnerStacks ? task . debugTask : null ,
1301+ ) ;
1302+ pushComponentStack ( suspendedFallbackTask ) ;
1303+ // TODO: This should be queued at a separate lower priority queue so that we only work
1304+ // on preparing fallbacks if we don't have any more main content to task on.
1305+ request . pingedTasks . push ( suspendedFallbackTask ) ;
12351306 }
1236- // We create suspended task for the fallback because we don't want to actually work
1237- // on it yet in case we finish the main content, so we queue for later.
1238- const suspendedFallbackTask = createRenderTask (
1239- request ,
1240- null ,
1241- fallback ,
1242- - 1 ,
1243- parentBoundary ,
1244- boundarySegment ,
1245- newBoundary . fallbackState ,
1246- fallbackAbortSet ,
1247- fallbackKeyPath ,
1248- task . formatContext ,
1249- task . context ,
1250- task . treeContext ,
1251- task . componentStack ,
1252- true ,
1253- ! disableLegacyContext ? task . legacyContext : emptyContextObject ,
1254- __DEV__ && enableOwnerStacks ? task . debugTask : null ,
1255- ) ;
1256- pushComponentStack ( suspendedFallbackTask ) ;
1257- // TODO: This should be queued at a separate lower priority queue so that we only work
1258- // on preparing fallbacks if we don't have any more main content to task on.
1259- request . pingedTasks . push ( suspendedFallbackTask ) ;
12601307}
12611308
12621309function replaySuspenseBoundary (
0 commit comments