Command
build
Is this a regression?
The previous version in which this bug was not present was
22.0.9
Description
After upgrading from Angular/Angular CLI 22.0.x to 22.1.x, a production build with all configured locales no longer completes on CI workers that previously had sufficient memory.
The i18n worker introduced in the 22.1 build path parses and transforms JavaScript with oxc-parser and MagicString. transformWithOxc() constructs a new OXC Visitor for each file/locale request. Its callbacks close over the per-request MagicString, source code, diagnostics, translation, and options.
OXC stores the callback-bearing { enter, exit } objects in a module-global cache, while its public Visitor wrapper does not reset that cache between instances. Completed file transforms therefore remain reachable for the lifetime of the Angular i18n worker. With full localization, retained heap grows approximately with output files × locales until Node terminates the worker at its heap limit.
This is not caused by Angular's persistent disk cache. Disabling cache reuse does not remove the per-process retention. Splitting locales across fresh processes hides the issue because process exit releases OXC's module-global cache; patching OXC so it does not retain visitor callbacks also allows the single-process full-localization build to complete.
The underlying OXC issue is tracked here: [OXC_ISSUE_URL]
Minimal Reproduction
-
Create or use an Angular application with build-time localization and enough compiled JavaScript to make the retained heap visible.
-
Configure multiple locales in angular.json. Our production application has 18 locales.
-
Install Angular/CLI 22.1.x (@angular/build 22.1.3 reproduces the problem with oxc-parser 0.142.0).
-
Run all locales in one process:
npx ng build <project> --configuration production --localize=true
-
Observe worker heap growth across file/locale requests and eventual termination.
-
Downgrade @angular/build to 22.0.9 and repeat. The same build completes.
The OXC retention can also be demonstrated independently of the application size with this small script:
import { randomBytes } from 'node:crypto';
import { MagicString } from 'magic-string';
import { parseSync, Visitor } from 'oxc-parser';
if (!global.gc) {
throw new Error('Run this script with node --expose-gc.');
}
global.gc();
const before = process.memoryUsage();
for (let index = 0; index < 40; index++) {
const payload = randomBytes(768 * 1024).toString('base64');
const code = `const payload = ${JSON.stringify(payload)}; tag\`message\`;`;
const { program } = parseSync(`chunk-${index}.js`, code);
const magicString = new MagicString(code);
new Visitor({
'TaggedTemplateExpression:exit'() {
return magicString.toString().length;
},
}).visit(program);
}
global.gc();
const after = process.memoryUsage();
const toMiB = (bytes) => Math.round(bytes / 1024 / 1024);
console.log({
heapDeltaMiB: toMiB(after.heapUsed - before.heapUsed),
externalDeltaMiB: toMiB(after.external - before.external),
rssDeltaMiB: toMiB(after.rss - before.rss),
});
Run with node --expose-gc repro.mjs. With oxc-parser 0.142.0, one observed result was:
{ heapDeltaMiB: 42, externalDeltaMiB: 2, rssDeltaMiB: 85 }
After changing OXC's getEnterExitObject() to return a fresh object instead of storing callback-bearing objects in the module-global cache, the same run produced:
{ heapDeltaMiB: 3, externalDeltaMiB: 2, rssDeltaMiB: 38 }
Exception or Error
The production build eventually fails with:
Error [ERR_WORKER_OUT_OF_MEMORY]: Worker terminated due to reaching memory limit: JS heap out of memory
During `ng serve`/watch mode, memory pressure can also leave the esbuild service in this downstream failure state:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
github.com/evanw/esbuild/internal/helpers.(*ThreadSafeWaitGroup).Wait(...)
github.com/evanw/esbuild/internal/helpers/waitgroup.go:36
main.runService.func2()
github.com/evanw/esbuild/cmd/esbuild/service.go:114 +0x8c
The worker out-of-memory error is the primary reproducible failure; the esbuild deadlock appears to be a secondary symptom after the Node side exits or stops servicing requests.
Your Environment
Angular CLI: 22.1.3
Angular: 22.1.1
@angular/build: 22.1.3
oxc-parser: 0.142.0
TypeScript: 6.0.3
Node: 22.22.3
npm: 10.9.8
OS: macOS arm64
Anything else relevant?
Angular source involved:
OXC source involved:
This is different from angular/angular-cli#33777, which concerns a Windows native-module unload crash and was resolved by a newer OXC release. This report is about JavaScript closure retention through the public Visitor cache and reproduces with OXC 0.142.0.
An OXC fix is the correct root fix. Angular can also protect its worker independently by creating one stateless Visitor per worker and routing callbacks through a short-lived active transform context that is assigned only around visitor.visit(program) and cleared in finally. That prevents OXC's long-lived cache from owning file-specific source state even when an affected OXC version is installed.
Command
build
Is this a regression?
The previous version in which this bug was not present was
22.0.9
Description
After upgrading from Angular/Angular CLI 22.0.x to 22.1.x, a production build with all configured locales no longer completes on CI workers that previously had sufficient memory.
The i18n worker introduced in the 22.1 build path parses and transforms JavaScript with
oxc-parserandMagicString.transformWithOxc()constructs a new OXCVisitorfor each file/locale request. Its callbacks close over the per-requestMagicString, source code, diagnostics, translation, and options.OXC stores the callback-bearing
{ enter, exit }objects in a module-global cache, while its publicVisitorwrapper does not reset that cache between instances. Completed file transforms therefore remain reachable for the lifetime of the Angular i18n worker. With full localization, retained heap grows approximately withoutput files × localesuntil Node terminates the worker at its heap limit.This is not caused by Angular's persistent disk cache. Disabling cache reuse does not remove the per-process retention. Splitting locales across fresh processes hides the issue because process exit releases OXC's module-global cache; patching OXC so it does not retain visitor callbacks also allows the single-process full-localization build to complete.
The underlying OXC issue is tracked here: [OXC_ISSUE_URL]
Minimal Reproduction
Create or use an Angular application with build-time localization and enough compiled JavaScript to make the retained heap visible.
Configure multiple locales in
angular.json. Our production application has 18 locales.Install Angular/CLI 22.1.x (
@angular/build22.1.3 reproduces the problem withoxc-parser0.142.0).Run all locales in one process:
Observe worker heap growth across file/locale requests and eventual termination.
Downgrade
@angular/buildto 22.0.9 and repeat. The same build completes.The OXC retention can also be demonstrated independently of the application size with this small script:
Run with
node --expose-gc repro.mjs. Withoxc-parser0.142.0, one observed result was:After changing OXC's
getEnterExitObject()to return a fresh object instead of storing callback-bearing objects in the module-global cache, the same run produced:Exception or Error
Your Environment
Anything else relevant?
Angular source involved:
packages/angular/build/src/tools/esbuild/i18n-inliner-worker.tsOXC source involved:
napi/parser/src-js/visit/index.jsnapi/parser/src-js/visit/visitor.jsThis is different from angular/angular-cli#33777, which concerns a Windows native-module unload crash and was resolved by a newer OXC release. This report is about JavaScript closure retention through the public
Visitorcache and reproduces with OXC 0.142.0.An OXC fix is the correct root fix. Angular can also protect its worker independently by creating one stateless
Visitorper worker and routing callbacks through a short-lived active transform context that is assigned only aroundvisitor.visit(program)and cleared infinally. That prevents OXC's long-lived cache from owning file-specific source state even when an affected OXC version is installed.