Repository: vercel/next.js
Pull request: Turbopack: Remove 20ms overhead on bootup

## What?

This fetch, even though not awaited, was kicked off eagerly and the first `fetch()` in Node.js loads the SSL certificate store which takes roughly 20ms on the main thread. Made sure that it's only fetched when the version is needed, which is the first time errors happen, and then cached from that point.


### Additional

In `test/integration/config-output-export/test/index.test.ts` nearly all tests are flaky because of the way they're structured to do asserts after finally that kills the application. 

This causes it to race: Either the assertion is correct before it quits or the assertion fails unexpectedly (flaking).

This PR fixes the test suite. Accidentally found this flakiness when I made the change in this PR that makes the version step take more than 50 milliseconds the first time. That caused the assertion to be consistently slower, thus reproducing the issue all the time.

Closes PACK-5417

Changed files:
filename: packages/next/src/server/dev/hot-reloader-turbopack.ts
patch: @@ -690,7 +690,17 @@ export async function createHotReloaderTurbopack(
     }),
   ]
 
-  const versionInfoPromise = getVersionInfo()
+  let versionInfoCached: ReturnType<typeof getVersionInfo> | undefined
+  // This fetch, even though not awaited, is not kicked off eagerly because the first `fetch()` in
+  // Node.js adds roughly 20ms main-thread blocking to load the SSL certificate cache
+  // We don't want that blocking time to be in the hot path for the `ready in` logging.
+  // Instead, the fetch is kicked off lazily when the first `getVersionInfoCached()` is called.
+  const getVersionInfoCached = (): ReturnType<typeof getVersionInfo> => {
+    if (!versionInfoCached) {
+      versionInfoCached = getVersionInfo()
+    }
+    return versionInfoCached
+  }
 
   let devtoolsFrontendUrl: string | undefined
   const nodeDebugType = getNodeDebugType()
@@ -910,7 +920,7 @@ export async function createHotReloaderTurbopack(
         }
 
         ;(async function () {
-          const versionInfo = await versionInfoPromise
+          const versionInfo = await getVersionInfoCached()
           const devToolsConfig = await getDevToolsConfig(distDir)
 
           const syncMessage: SyncMessage = {

filename: test/integration/config-output-export/test/index.test.ts
patch: @@ -161,14 +161,14 @@ describe('config-output-export', () => {
         output: 'export',
       })
       response = await fetchViaHTTP(result.port, '/api/wow')
+      expect(response.status).toBe(404)
+      expect(result?.stderr).toContain(
+        'API Routes cannot be used with "output: export".'
+      )
     } finally {
       await killApp(app).catch(() => {})
       fs.rmSync(pagesApi, { recursive: true, force: true })
     }
-    expect(response.status).toBe(404)
-    expect(result?.stderr).toContain(
-      'API Routes cannot be used with "output: export".'
-    )
   })
 
   it('should error with middleware function', async () => {
@@ -184,15 +184,15 @@ describe('config-output-export', () => {
         output: 'export',
       })
       response = await fetchViaHTTP(result.port, '/api/mw')
+      expect(response.status).toBe(404)
+      expect(result?.stdout + result?.stderr).not.toContain('[mw]')
+      expect(result?.stderr).toContain(
+        'Middleware cannot be used with "output: export".'
+      )
     } finally {
       await killApp(app).catch(() => {})
       fs.rmSync(middleware)
     }
-    expect(response.status).toBe(404)
-    expect(result?.stdout + result?.stderr).not.toContain('[mw]')
-    expect(result?.stderr).toContain(
-      'Middleware cannot be used with "output: export".'
-    )
   })
 
   it('should error with getStaticProps and revalidate 10 seconds (ISR)', async () => {
@@ -217,17 +217,18 @@ describe('config-output-export', () => {
         output: 'export',
       })
       browser = await webdriver(result.port, '/blog')
+
+      await assertHasRedbox(browser)
+      expect(await getRedboxHeader(browser)).toContain(
+        'ISR cannot be used with "output: export".'
+      )
+      expect(result?.stderr).toContain(
+        'ISR cannot be used with "output: export".'
+      )
     } finally {
       await killApp(app).catch(() => {})
       fs.rmSync(blog)
     }
-    await assertHasRedbox(browser)
-    expect(await getRedboxHeader(browser)).toContain(
-      'ISR cannot be used with "output: export".'
-    )
-    expect(result?.stderr).toContain(
-      'ISR cannot be used with "output: export".'
-    )
   })
 
   it('should work with getStaticProps and revalidate false', async () => {
@@ -252,11 +253,11 @@ describe('config-output-export', () => {
         output: 'export',
       })
       browser = await webdriver(result.port, '/blog')
+      await assertNoRedbox(browser)
     } finally {
       await killApp(app).catch(() => {})
       fs.rmSync(blog)
     }
-    await assertNoRedbox(browser)
   })
 
   it('should work with getStaticProps and without revalidate', async () => {
@@ -280,11 +281,11 @@ describe('config-output-export', () => {
         output: 'export',
       })
       browser = await webdriver(result.port, '/blog')
+      await assertNoRedbox(browser)
     } finally {
       await killApp(app).catch(() => {})
       fs.rmSync(blog)
     }
-    await assertNoRedbox(browser)
   })
 
   it('should error with getServerSideProps without fallback', async () => {
@@ -308,17 +309,17 @@ describe('config-output-export', () => {
         output: 'export',
       })
       browser = await webdriver(result.port, '/blog')
+      await assertHasRedbox(browser)
+      expect(await getRedboxHeader(browser)).toContain(
+        'getServerSideProps cannot be used with "output: export".'
+      )
+      expect(result?.stderr).toContain(
+        'getServerSideProps cannot be used with "output: export".'
+      )
     } finally {
       await killApp(app).catch(() => {})
       fs.rmSync(blog)
     }
-    await assertHasRedbox(browser)
-    expect(await getRedboxHeader(browser)).toContain(
-      'getServerSideProps cannot be used with "output: export".'
-    )
-    expect(result?.stderr).toContain(
-      'getServerSideProps cannot be used with "output: export".'
-    )
   })
 
   it('should error with getStaticPaths and fallback true', async () => {
@@ -440,13 +441,13 @@ describe('config-output-export', () => {
         output: 'export',
       })
       browser = await webdriver(result.port, '/posts/one')
+      const h1 = await browser.elementByCss('h1')
+      expect(await h1.text()).toContain('Hello from one')
+      await assertNoRedbox(browser)
+      expect(result.stderr).toBeEmpty()
     } finally {
       await killApp(app).catch(() => {})
       fs.rmSync(posts, { recursive: true, force: true })
     }
-    const h1 = await browser.elementByCss('h1')
-    expect(await h1.text()).toContain('Hello from one')
-    await assertNoRedbox(browser)
-    expect(result.stderr).toBeEmpty()
   })
 })