Skip to content

Commit 4fd8140

Browse files
committed
perf(@angular/build): release build resources early in non-watch mode (#33715)
Disposes the Angular compilation and bundler contexts within the build execution for non-watch builds once bundling has completed. This terminates the TypeScript compilation worker and JavaScript transformation workers before the post-bundle steps (chunk optimization, i18n inlining, index generation) execute instead of after all results have been emitted. Repeat compilation disposal calls now return the in progress disposal to ensure all callers can await completion of the underlying compiler shutdown. The result disposal also releases the incremental build caches which can retain the emitted contents of every TypeScript file within the program. This reduces the peak memory usage of the build for both the non-watch early disposal and watch mode teardown. Closes #33368 PR Close #33715
1 parent 917d3b2 commit 4fd8140

5 files changed

Lines changed: 52 additions & 12 deletions

File tree

packages/angular/build/src/builders/application/execute-build.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,17 @@ export async function executeBuild(
156156
executionResult.extraWatchFiles.push(...componentStyleBundler.collectReferencedFiles());
157157
}
158158

159+
// In non-watch mode, the bundler rebuild contexts and Angular compilation
160+
// are no longer needed once bundling completes. Dispose them early to free
161+
// worker processes, TypeScript ASTs, and caches before post-bundling steps
162+
// execute or before returning bundling errors.
163+
if (!options.watch) {
164+
// This is fire-and-forget so worker teardown runs concurrently in the background
165+
// without blocking the critical path of post-bundle optimization steps.
166+
// Any in-flight disposal is awaited in the builder action's finally block.
167+
void Promise.allSettled([angularCompilationContext?.dispose(), executionResult.dispose()]);
168+
}
169+
159170
// Return if the bundling has errors
160171
if (bundlingResult.errors) {
161172
executionResult.addErrors(bundlingResult.errors);

packages/angular/build/src/tools/esbuild/angular/compilation-state.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,14 @@ export class AngularCompilationContext {
4646
this.#pendingCompilation = true;
4747
}
4848

49-
#disposed = false;
49+
#disposal: Promise<void> | undefined;
5050

51-
async dispose(): Promise<void> {
52-
if (this.#disposed) {
53-
return;
54-
}
55-
this.#disposed = true;
51+
dispose(): Promise<void> {
52+
// Reuse any in progress disposal to ensure all callers can await completion
53+
return (this.#disposal ??= this.#close());
54+
}
55+
56+
async #close(): Promise<void> {
5657
this.markAsReady(true);
5758
try {
5859
await this.#compilation.close?.();

packages/angular/build/src/tools/esbuild/angular/source-file-cache.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,19 @@ export class SourceFileCache extends Map<string, ts.SourceFile> {
2525
super();
2626
}
2727

28+
/**
29+
* Releases all cached content. The cached data is only needed for incremental
30+
* rebuilds and can include the emitted contents of every TypeScript file in the
31+
* program. The cache is repopulated if a build is performed after this is called.
32+
*/
33+
dispose(): void {
34+
this.clear();
35+
this.modifiedFiles.clear();
36+
this.typeScriptFileCache.clear();
37+
this.loadResultCache.clear();
38+
this.referencedFiles = undefined;
39+
}
40+
2841
invalidate(files: Iterable<string>): boolean {
2942
if (files !== this.modifiedFiles) {
3043
this.modifiedFiles.clear();

packages/angular/build/src/tools/esbuild/bundler-execution-result.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -194,11 +194,21 @@ export class ExecutionResult {
194194
return changed;
195195
}
196196

197-
async dispose(): Promise<void> {
198-
await Promise.allSettled([
199-
...this.rebuildContexts.typescriptContexts.map((context) => context.dispose()),
200-
...this.rebuildContexts.otherContexts.map((context) => context.dispose()),
201-
this.componentStyleBundler.dispose(),
202-
]);
197+
#disposal?: Promise<void>;
198+
199+
dispose(): Promise<void> {
200+
return (this.#disposal ??= this.#dispose());
201+
}
202+
203+
async #dispose(): Promise<void> {
204+
try {
205+
await Promise.allSettled([
206+
...this.rebuildContexts.typescriptContexts.map((context) => context.dispose()),
207+
...this.rebuildContexts.otherContexts.map((context) => context.dispose()),
208+
this.componentStyleBundler.dispose(),
209+
]);
210+
} finally {
211+
this.codeBundleCache?.dispose();
212+
}
203213
}
204214
}

packages/angular/build/src/tools/esbuild/load-result-cache.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,4 +90,9 @@ export class MemoryLoadResultCache implements LoadResultCache {
9090
// are namespaced request paths and not disk-based file paths.
9191
return [...this.#fileDependencies.keys()];
9292
}
93+
94+
clear(): void {
95+
this.#loadResults.clear();
96+
this.#fileDependencies.clear();
97+
}
9398
}

0 commit comments

Comments
 (0)