Repository: vercel/next.js
Pull request: [dynamicIO] only abort once per prerender

Prior to this change multiple sync dynamic APIs could trigger multiple aborts. Generally this isn't a problem however it is wasteful compute and blocks a later refactor where we stop explicitly tracking whether the sync dynamic access is what caused the prerender to abort or not. To achieve better perf and future semantics we now only abort if the prerender is not yet aborted. In effect this means that there can only be one sync abort error per prerender in RSC and SSR respectively.

Changed files:
filename: packages/next/src/server/app-render/dynamic-rendering.ts
patch: @@ -294,7 +294,7 @@ export function abortOnSynchronousPlatformIOAccess(
       dynamicTracking.syncDynamicErrorWithStack = errorWithStack
     }
   }
-  return abortOnSynchronousDynamicDataAccess(route, expression, prerenderStore)
+  abortOnSynchronousDynamicDataAccess(route, expression, prerenderStore)
 }
 
 export function trackSynchronousPlatformIOAccessInDev(
@@ -321,19 +321,27 @@ export function abortAndThrowOnSynchronousRequestDataAccess(
   errorWithStack: Error,
   prerenderStore: PrerenderStoreModern
 ): never {
-  const dynamicTracking = prerenderStore.dynamicTracking
-  if (dynamicTracking) {
-    if (dynamicTracking.syncDynamicErrorWithStack === null) {
-      dynamicTracking.syncDynamicExpression = expression
-      dynamicTracking.syncDynamicErrorWithStack = errorWithStack
-      if (prerenderStore.validating === true) {
-        // We always log Request Access in dev at the point of calling the function
-        // So we mark the dynamic validation as not requiring it to be printed
-        dynamicTracking.syncDynamicLogged = true
+  const prerenderSignal = prerenderStore.controller.signal
+  if (prerenderSignal.aborted === false) {
+    // TODO it would be better to move this aborted check into the callsite so we can avoid making
+    // the error object when it isn't relevant to the aborting of the prerender however
+    // since we need the throw semantics regardless of whether we abort it is easier to land
+    // this way. See how this was handled with `abortOnSynchronousPlatformIOAccess` for a closer
+    // to ideal implementation
+    const dynamicTracking = prerenderStore.dynamicTracking
+    if (dynamicTracking) {
+      if (dynamicTracking.syncDynamicErrorWithStack === null) {
+        dynamicTracking.syncDynamicExpression = expression
+        dynamicTracking.syncDynamicErrorWithStack = errorWithStack
+        if (prerenderStore.validating === true) {
+          // We always log Request Access in dev at the point of calling the function
+          // So we mark the dynamic validation as not requiring it to be printed
+          dynamicTracking.syncDynamicLogged = true
+        }
       }
     }
+    abortOnSynchronousDynamicDataAccess(route, expression, prerenderStore)
   }
-  abortOnSynchronousDynamicDataAccess(route, expression, prerenderStore)
   throw createPrerenderInterruptedError(
     `Route ${route} needs to bail out of prerendering at this point because it used ${expression}.`
   )

filename: packages/next/src/server/node-environment-extensions/utils.tsx
patch: @@ -12,32 +12,37 @@ export function io(expression: string, type: ApiType) {
   const workUnitStore = workUnitAsyncStorage.getStore()
   if (workUnitStore) {
     if (workUnitStore.type === 'prerender') {
-      const workStore = workAsyncStorage.getStore()
-      if (workStore) {
-        let message: string
-        switch (type) {
-          case 'time':
-            message = `Route "${workStore.route}" used ${expression} instead of using \`performance\` or without explicitly calling \`await connection()\` beforehand. See more info here: https://nextjs.org/docs/messages/next-prerender-current-time`
-            break
-          case 'random':
-            message = `Route "${workStore.route}" used ${expression} outside of \`"use cache"\` and without explicitly calling \`await connection()\` beforehand. See more info here: https://nextjs.org/docs/messages/next-prerender-random`
-            break
-          case 'crypto':
-            message = `Route "${workStore.route}" used ${expression} outside of \`"use cache"\` and without explicitly calling \`await connection()\` beforehand. See more info here: https://nextjs.org/docs/messages/next-prerender-crypto`
-            break
-          default:
-            throw new InvariantError(
-              'Unknown expression type in abortOnSynchronousPlatformIOAccess.'
-            )
-        }
-        const errorWithStack = new Error(message)
+      const prerenderSignal = workUnitStore.controller.signal
+      if (prerenderSignal.aborted === false) {
+        // If the prerender signal is already aborted we don't need to construct any stacks
+        // because something else actually terminated the prerender.
+        const workStore = workAsyncStorage.getStore()
+        if (workStore) {
+          let message: string
+          switch (type) {
+            case 'time':
+              message = `Route "${workStore.route}" used ${expression} instead of using \`performance\` or without explicitly calling \`await connection()\` beforehand. See more info here: https://nextjs.org/docs/messages/next-prerender-current-time`
+              break
+            case 'random':
+              message = `Route "${workStore.route}" used ${expression} outside of \`"use cache"\` and without explicitly calling \`await connection()\` beforehand. See more info here: https://nextjs.org/docs/messages/next-prerender-random`
+              break
+            case 'crypto':
+              message = `Route "${workStore.route}" used ${expression} outside of \`"use cache"\` and without explicitly calling \`await connection()\` beforehand. See more info here: https://nextjs.org/docs/messages/next-prerender-crypto`
+              break
+            default:
+              throw new InvariantError(
+                'Unknown expression type in abortOnSynchronousPlatformIOAccess.'
+              )
+          }
+          const errorWithStack = new Error(message)
 
-        abortOnSynchronousPlatformIOAccess(
-          workStore.route,
-          expression,
-          errorWithStack,
-          workUnitStore
-        )
+          abortOnSynchronousPlatformIOAccess(
+            workStore.route,
+            expression,
+            errorWithStack,
+            workUnitStore
+          )
+        }
       }
     } else if (
       workUnitStore.type === 'request' &&