Repository: vercel/next.js
Pull request: [turbopack] Clear chunk cache on HMR instead of creating new `next-server` VM

Closes https://linear.app/vercel/issue/NAR-207


React Server has multiple side-effects during module evaluation (console patching and async_hooks) without a dispose hook it can hook into to clean up these side-effects. Unfortunately, Turbopack needed to re-require the `next-server` runtime on HMR to clean up React Client's chunk cache. This resulted in registering async_hooks for each HMR update. This always leaked some memory but wasn't catastrophic because it just created deeper stacks in Console calls. async_hooks are a much hotter path so it become more noticeable.

Now we only load the `next-server` runtime once so these additional modules no longer need to be kept in memory:

https://github.com/user-attachments/assets/44682e89-f9ea-4934-9b49-423ff326bf1a



It still leaks for HMR updates if you edit `next-server` while keeping the dev server running. But this is only our DX problem and you probably want to restart the dev server anyway when you edited `next-server`.

## Test plan

With https://github.com/facebook/react/pull/33792 (e.g. in https://github.com/vercel/next.js/pull/81680)
```
pnpm debug dev test/e2e/app-dir/hello-world --turbopack
# goto http://localhost:3000/
# Edit dev test/e2e/app-dir/hello-world/app/page.tsx multiple times (actually save for each edit)
# memory snapshot
# modules no longer leak
```

Changed files:
filename: packages/next/src/server/dev/hot-reloader-turbopack.ts
patch: @@ -37,7 +37,7 @@ import {
 } from './middleware-turbopack'
 import { PageNotFoundError } from '../../shared/lib/utils'
 import { debounce } from '../utils'
-import { deleteCache, deleteFromRequireCache } from './require-cache'
+import { deleteCache } from './require-cache'
 import {
   clearAllModuleContexts,
   clearModuleContext,
@@ -342,30 +342,12 @@ export async function createHotReloaderTurbopack(
 
     resetFetch()
 
-    const hasAppPaths = writtenEndpoint.serverPaths.some(({ path: p }) =>
-      p.startsWith('server/app')
-    )
-
     // Edge uses the browser runtime which already disposes chunks individually.
     // TODO: process.env.NEXT_RUNTIME is 'nodejs' even though Node.js runtime is not used.
     if ('__turbopack_clear_chunk_cache__' in globalThis) {
       ;(globalThis as any).__turbopack_clear_chunk_cache__()
     }
 
-    // TODO: Stop re-evaluating React Client once it relies on Turbopack's chunk cache.
-    if (hasAppPaths) {
-      deleteFromRequireCache(
-        require.resolve(
-          'next/dist/compiled/next-server/app-page-turbo.runtime.dev.js'
-        )
-      )
-      deleteFromRequireCache(
-        require.resolve(
-          'next/dist/compiled/next-server/app-page-turbo-experimental.runtime.dev.js'
-        )
-      )
-    }
-
     const serverPaths = writtenEndpoint.serverPaths.map(({ path: p }) =>
       join(distDir, p)
     )

filename: packages/next/src/server/dev/require-cache.ts
patch: @@ -2,7 +2,7 @@ import isError from '../../lib/is-error'
 import { realpathSync } from '../../lib/realpath'
 import { clearManifestCache } from '../load-manifest.external'
 
-export function deleteFromRequireCache(filePath: string) {
+function deleteFromRequireCache(filePath: string) {
   try {
     filePath = realpathSync(filePath)
   } catch (e) {