Batch require cache deletion to avoid quadratic scanning#90625
Open
lukesandberg wants to merge 1 commit intocanaryfrom
Open
Batch require cache deletion to avoid quadratic scanning#90625lukesandberg wants to merge 1 commit intocanaryfrom
lukesandberg wants to merge 1 commit intocanaryfrom
Conversation
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).
be36cf9 to
a6dfc86
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What?
Rewrites
deleteFromRequireCache()inpackages/next/src/server/dev/require-cache.tsto accept an array of file paths and perform a single scan ofrequire.cache, instead of one full scan per file. Adds adeleteCacheBatch()export and updates all three batch call sites to use it.Why?
During server-side HMR,
deleteCache()is called in loops — once per server path in the turbopack hot-reloader (5-20 files), up to 15 times perwriteManifests()in the manifest-loader, and 2 + N times (runtime chunks + page entries) in the webpack plugin'safterEmithook. Each call scans the entirerequire.cacheto clean up parent-child references, making the overall cost O(N × C) where C is the cache size. In large apps with thousands of cached modules, this becomes a meaningful bottleneck on every HMR update.How?
Core change (
require-cache.ts):deleteFromRequireCachenow acceptsstring[]. It resolves all paths upfront, collects target modules into aSet<NodeModule>, then does one scan ofrequire.cacheusingSet.has()(O(1) lookup) to filter children arrays. For single-item callers (deleteCache), the overhead of a 1-element Set is negligible.Call site updates:
hot-reloader-turbopack.ts: Collects files to delete into an array during theserverPathsloop, callsdeleteCacheBatch()once after.clearModuleContextstays per-file (separate system).manifest-loader.ts: AddspendingCacheDeletesarray toTurbopackManifestLoader. All ~15deleteCache()calls inwrite*methods becomepush()calls. Flushed at the end ofwriteManifests()with a singledeleteCacheBatch(). Moving cache deletion to after allwriteFileAtomiccalls is safe (synchronous code, no interleaving) and slightly better (new files on disk before cache is cleared).nextjs-require-cache-hot-reloader.ts(webpack): Batches theafterEmithook — collects runtime chunk + page entry paths, callsdeleteCacheBatch()once. TheassetEmittedtap callback stays as individualdeleteCache().Theoretical improvement:
clearRequireCachewriteManifestsafterEmitBenchmark
Tested with a generated 250-route app with 50 API route handlers importing server external packages (zod, lodash, express, pg, ioredis) plus middleware. API route handlers are key because unlike bundled app-router pages, they create real
require.cachedepth with native Node.js module loading.Results (steady-state, 18 paths / 7 found / 781 nodes / ~1800 edges):
Raw data (representative samples):
The speedup scales with cache complexity — in production apps with more server externals (ORMs, validation libs, etc.) the cache would be larger and the improvement proportionally greater. With only bundled app-router pages (few edges in require.cache graph), the difference is negligible since most of the graph complexity comes from native Node.js module loading.