Skip to content

Commit a6dfc86

Browse files
committed
Batch require cache deletion to avoid quadratic scanning
deleteFromRequireCache() scans the entire require.cache for every single module deletion to clean up parent-child references. When called in a loop for N files, this is O(N * C) where C is the cache size. Rewrite deleteFromRequireCache to accept string[], collecting modules into a Set and doing one scan with O(1) Set.has() lookups. Add deleteCacheBatch() for callers that delete multiple modules. Update all three batch call sites: turbopack hot-reloader (serverPaths loop), manifest-loader (~15 manifest files per writeManifests), and webpack plugin (afterEmit runtime + page entries).
1 parent 0db7804 commit a6dfc86

File tree

4 files changed

+80
-50
lines changed

4 files changed

+80
-50
lines changed

packages/next/src/build/webpack/plugins/nextjs-require-cache-hot-reloader.ts

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,14 @@ export class NextJsRequireCacheHotReloader implements WebpackPluginInstance {
2222
compiler.hooks.assetEmitted.tap(PLUGIN_NAME, (_file, { targetPath }) => {
2323
// Clear module context in this process
2424
clearModuleContext(targetPath)
25-
deleteCache(targetPath)
25+
deleteCache([targetPath])
2626
})
2727

2828
compiler.hooks.afterEmit.tapPromise(PLUGIN_NAME, async (compilation) => {
29+
const allPaths: string[] = []
30+
2931
for (const name of RUNTIME_NAMES) {
30-
const runtimeChunkPath = path.join(
31-
compilation.outputOptions.path!,
32-
`${name}.js`
33-
)
34-
deleteCache(runtimeChunkPath)
32+
allPaths.push(path.join(compilation.outputOptions.path!, `${name}.js`))
3533
}
3634

3735
// we need to make sure to clear all server entries from cache
@@ -43,12 +41,10 @@ export class NextJsRequireCacheHotReloader implements WebpackPluginInstance {
4341
})
4442

4543
for (const page of entries) {
46-
const outputPath = path.join(
47-
compilation.outputOptions.path!,
48-
page + '.js'
49-
)
50-
deleteCache(outputPath)
44+
allPaths.push(path.join(compilation.outputOptions.path!, page + '.js'))
5145
}
46+
47+
deleteCache(allPaths)
5248
})
5349
}
5450
}

packages/next/src/server/dev/hot-reloader-turbopack.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,7 @@ export async function createHotReloaderTurbopack(
592592
// use the edge runtime, and App Router edge routes all don't support server HMR.
593593
const usesServerHmr = entryType === 'app' && writtenEndpoint.type !== 'edge'
594594

595+
const filesToDelete: string[] = []
595596
for (const file of serverPaths) {
596597
const relativePath = relative(distDir, file)
597598

@@ -603,11 +604,12 @@ export async function createHotReloaderTurbopack(
603604
}
604605

605606
clearModuleContext(file)
606-
// For Pages Router, edge routes, middleware, and manifest files
607-
// (e.g., *_client-reference-manifest.js): clear the sharedCache in
608-
// evalManifest(), Node.js require.cache, and edge runtime module contexts.
609-
deleteCache(file)
607+
filesToDelete.push(file)
610608
}
609+
// For Pages Router, edge routes, middleware, and manifest files
610+
// (e.g., *_client-reference-manifest.js): clear the sharedCache in
611+
// evalManifest(), Node.js require.cache, and edge runtime module contexts.
612+
deleteCache(filesToDelete)
611613

612614
// Clear Turbopack's chunk-loading cache so chunks are re-required from disk on
613615
// the next request.

packages/next/src/server/dev/require-cache.ts

Lines changed: 45 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,59 @@ import isError from '../../lib/is-error'
22
import { realpathSync } from '../../lib/realpath'
33
import { clearManifestCache } from '../load-manifest.external'
44

5-
function deleteFromRequireCache(filePath: string) {
6-
try {
7-
filePath = realpathSync(filePath)
8-
} catch (e) {
9-
if (isError(e) && e.code !== 'ENOENT') throw e
5+
/**
6+
* Batch delete modules from require.cache with a single scan.
7+
*
8+
* When deleting N modules, this performs ONE scan of require.cache
9+
* instead of N scans, reducing complexity from O(N * C) to O(C + N)
10+
* where C = size of require.cache.
11+
*/
12+
function deleteFromRequireCache(filePaths: string[]): void {
13+
// Phase 1: Resolve all paths and collect modules to delete
14+
const resolvedPaths: string[] = []
15+
const modsToDelete = new Set<NodeModule>()
16+
17+
for (let filePath of filePaths) {
18+
try {
19+
filePath = realpathSync(filePath)
20+
} catch (e) {
21+
if (isError(e) && e.code !== 'ENOENT') throw e
22+
}
23+
const mod = require.cache[filePath]
24+
if (mod) {
25+
resolvedPaths.push(filePath)
26+
modsToDelete.add(mod)
27+
}
1028
}
11-
const mod = require.cache[filePath]
12-
if (mod) {
13-
// remove the child reference from all parent modules
14-
for (const parent of Object.values(require.cache)) {
15-
if (parent?.children) {
16-
const idx = parent.children.indexOf(mod)
17-
if (idx >= 0) parent.children.splice(idx, 1)
29+
30+
if (modsToDelete.size === 0) return
31+
32+
// Phase 2: Single scan of require.cache to remove child references
33+
for (const parent of Object.values(require.cache)) {
34+
if (parent?.children) {
35+
for (let i = parent.children.length - 1; i >= 0; i--) {
36+
if (modsToDelete.has(parent.children[i])) {
37+
parent.children.splice(i, 1)
38+
}
1839
}
1940
}
20-
// remove parent references from external modules
41+
}
42+
43+
// Phase 3: Clear parent references from children and delete cache entries
44+
for (const mod of modsToDelete) {
2145
for (const child of mod.children) {
2246
child.parent = null
2347
}
48+
}
49+
50+
for (const filePath of resolvedPaths) {
2451
delete require.cache[filePath]
25-
return true
2652
}
27-
return false
2853
}
2954

30-
export function deleteCache(filePath: string) {
31-
// try to clear it from the fs cache
32-
clearManifestCache(filePath)
33-
34-
deleteFromRequireCache(filePath)
55+
export function deleteCache(filePaths: string[]) {
56+
for (const filePath of filePaths) {
57+
clearManifestCache(filePath)
58+
}
59+
deleteFromRequireCache(filePaths)
3560
}

packages/next/src/shared/lib/turbopack/manifest-loader.ts

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ export class TurbopackManifestLoader {
209209
/// interceptionRewrites that have been written to disk
210210
/// This is used to avoid unnecessary writes if the rewrites haven't changed
211211
private cachedInterceptionRewrites: string | undefined = undefined
212+
private pendingCacheDeletes: string[] = []
212213

213214
private readonly distDir: string
214215
private readonly buildId: string
@@ -318,8 +319,8 @@ export class TurbopackManifestLoader {
318319
`${SERVER_REFERENCE_MANIFEST}.js`
319320
)
320321
const json = JSON.stringify(actionManifest, null, 2)
321-
deleteCache(actionManifestJsonPath)
322-
deleteCache(actionManifestJsPath)
322+
this.pendingCacheDeletes.push(actionManifestJsonPath)
323+
this.pendingCacheDeletes.push(actionManifestJsPath)
323324
writeFileAtomic(actionManifestJsonPath, json)
324325
writeFileAtomic(
325326
actionManifestJsPath,
@@ -351,7 +352,7 @@ export class TurbopackManifestLoader {
351352
'server',
352353
APP_PATHS_MANIFEST
353354
)
354-
deleteCache(appPathsManifestPath)
355+
this.pendingCacheDeletes.push(appPathsManifestPath)
355356
writeFileAtomic(
356357
appPathsManifestPath,
357358
JSON.stringify(appPathsManifest, null, 2)
@@ -364,7 +365,7 @@ export class TurbopackManifestLoader {
364365
}
365366
const webpackStats = this.mergeWebpackStats(this.webpackStats.values())
366367
const path = join(this.distDir, 'server', WEBPACK_STATS)
367-
deleteCache(path)
368+
this.pendingCacheDeletes.push(path)
368369
writeFileAtomic(path, JSON.stringify(webpackStats, null, 2))
369370
}
370371

@@ -383,8 +384,8 @@ export class TurbopackManifestLoader {
383384
'server',
384385
`${SUBRESOURCE_INTEGRITY_MANIFEST}.js`
385386
)
386-
deleteCache(pathJson)
387-
deleteCache(pathJs)
387+
this.pendingCacheDeletes.push(pathJson)
388+
this.pendingCacheDeletes.push(pathJs)
388389
writeFileAtomic(pathJson, JSON.stringify(sriManifest, null, 2))
389390
writeFileAtomic(
390391
pathJs,
@@ -560,7 +561,7 @@ export class TurbopackManifestLoader {
560561
'server',
561562
`${INTERCEPTION_ROUTE_REWRITE_MANIFEST}.js`
562563
)
563-
deleteCache(interceptionRewriteManifestPath)
564+
this.pendingCacheDeletes.push(interceptionRewriteManifestPath)
564565

565566
writeFileAtomic(
566567
interceptionRewriteManifestPath,
@@ -586,8 +587,8 @@ export class TurbopackManifestLoader {
586587
`${MIDDLEWARE_BUILD_MANIFEST}.js`
587588
)
588589

589-
deleteCache(buildManifestPath)
590-
deleteCache(middlewareBuildManifestPath)
590+
this.pendingCacheDeletes.push(buildManifestPath)
591+
this.pendingCacheDeletes.push(middlewareBuildManifestPath)
591592
writeFileAtomic(buildManifestPath, JSON.stringify(buildManifest, null, 2))
592593
writeFileAtomic(
593594
middlewareBuildManifestPath,
@@ -606,7 +607,7 @@ export class TurbopackManifestLoader {
606607
this.distDir,
607608
`fallback-${BUILD_MANIFEST}`
608609
)
609-
deleteCache(fallbackBuildManifestPath)
610+
this.pendingCacheDeletes.push(fallbackBuildManifestPath)
610611
writeFileAtomic(
611612
fallbackBuildManifestPath,
612613
JSON.stringify(fallbackBuildManifest, null, 2)
@@ -727,8 +728,8 @@ export class TurbopackManifestLoader {
727728
'server',
728729
`${NEXT_FONT_MANIFEST}.js`
729730
)
730-
deleteCache(fontManifestJsonPath)
731-
deleteCache(fontManifestJsPath)
731+
this.pendingCacheDeletes.push(fontManifestJsonPath)
732+
this.pendingCacheDeletes.push(fontManifestJsPath)
732733
writeFileAtomic(fontManifestJsonPath, json)
733734
writeFileAtomic(
734735
fontManifestJsPath,
@@ -872,7 +873,7 @@ export class TurbopackManifestLoader {
872873
'server',
873874
MIDDLEWARE_MANIFEST
874875
)
875-
deleteCache(middlewareManifestPath)
876+
this.pendingCacheDeletes.push(middlewareManifestPath)
876877
writeFileAtomic(
877878
middlewareManifestPath,
878879
JSON.stringify(middlewareManifest, null, 2)
@@ -888,7 +889,7 @@ export class TurbopackManifestLoader {
888889
2
889890
)};self.__MIDDLEWARE_MATCHERS_CB && self.__MIDDLEWARE_MATCHERS_CB()`
890891

891-
deleteCache(clientMiddlewareManifestPath)
892+
this.pendingCacheDeletes.push(clientMiddlewareManifestPath)
892893
writeFileAtomic(
893894
join(this.distDir, clientMiddlewareManifestPath),
894895
clientMiddlewareManifestJs
@@ -928,7 +929,7 @@ export class TurbopackManifestLoader {
928929
}
929930
const pagesManifest = this.mergePagesManifests(this.pagesManifests.values())
930931
const pagesManifestPath = join(this.distDir, 'server', PAGES_MANIFEST)
931-
deleteCache(pagesManifestPath)
932+
this.pendingCacheDeletes.push(pagesManifestPath)
932933
writeFileAtomic(pagesManifestPath, JSON.stringify(pagesManifest, null, 2))
933934
}
934935

@@ -959,6 +960,12 @@ export class TurbopackManifestLoader {
959960
if (process.env.TURBOPACK_STATS != null) {
960961
this.writeWebpackStats()
961962
}
963+
964+
// Flush all queued cache deletions in a single require.cache scan
965+
if (this.pendingCacheDeletes.length > 0) {
966+
deleteCache(this.pendingCacheDeletes)
967+
this.pendingCacheDeletes = []
968+
}
962969
}
963970
}
964971

0 commit comments

Comments
 (0)