Skip to content

Commit 2d08f1a

Browse files
steveclaude
andcommitted
fix: cache compiled modules to prevent per-request SourceTextModule leak
The buildLink() function had a cache for Node builtins but none for regular imports. Every dynamic import() during SSR rendering compiled a new vm.SourceTextModule, read the file from disk, and evaluated it. These modules were never freed because they're linked to the vm.Context. After ~30 SSR renders, accumulated SourceTextModule instances filled the 384MB heap limit and the worker OOMed. The stack traces confirmed this: OOM consistently occurred inside ModuleWrap::CompileSourceTextModule and SourceTextModule::Evaluate. The fix: cache compiled modules by resolved file path. A module compiled once is reused for all subsequent imports of the same file. This is safe because the file contents don't change at runtime, and all modules share the same vm.Context. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
1 parent 6765c6b commit 2d08f1a

1 file changed

Lines changed: 8 additions & 3 deletions

File tree

src/ember-app.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -243,10 +243,10 @@ export default class EmberApp {
243243
const importModuleDynamically = async specifier => {
244244
return (await link(specifier)).namespace;
245245
};
246-
const builtinCache = new Map();
246+
const moduleCache = new Map();
247247
const link = async (specifier, referencingModule) => {
248248
if (nodeBuiltins.has(specifier)) {
249-
if (builtinCache.has(specifier)) return builtinCache.get(specifier);
249+
if (moduleCache.has(specifier)) return moduleCache.get(specifier);
250250
const canonical = specifier.startsWith('node:') ? specifier : `node:${specifier}`;
251251
const native = await import(canonical);
252252
const exportNames = Object.keys(native);
@@ -262,15 +262,20 @@ export default class EmberApp {
262262
);
263263
await synth.link(() => {});
264264
await synth.evaluate();
265-
builtinCache.set(specifier, synth);
265+
moduleCache.set(specifier, synth);
266266
return synth;
267267
}
268268
const base = referencingModule?.identifier || defaultBase;
269269
const identifier = await this.resolveImport(specifier, base);
270+
// Cache compiled modules by resolved path. Without this, every
271+
// dynamic import during SSR compiles a new SourceTextModule,
272+
// which are never freed and accumulate until OOM.
273+
if (moduleCache.has(identifier)) return moduleCache.get(identifier);
270274
const module = await this.buildScript(
271275
identifier, context, link, importModuleDynamically,
272276
);
273277
await module.evaluate();
278+
moduleCache.set(identifier, module);
274279
return module;
275280
};
276281
return { link, importModuleDynamically };

0 commit comments

Comments
 (0)