From e60eb4313184972543eeea6c42714cdd6b7b235e Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Mon, 28 Sep 2026 17:32:16 +0000 Subject: [PATCH 1/5] perf(@angular/build): cache transformed configuration in TypeScript compilation When `rootFiles` are explicitly provided (such as in library builds), avoid re-reading and re-parsing `tsconfig.json` and traversing the project directory structure on every watch rebuild. Cache the `TransformedConfiguration` and invalidate it only when a `.json` configuration file changes. --- .../angular/compilation/aot-compilation.ts | 36 ++++--- .../angular/compilation/jit-compilation.ts | 8 +- .../compilation/typescript-compilation.ts | 102 ++++++++++++++---- 3 files changed, 108 insertions(+), 38 deletions(-) diff --git a/packages/angular/build/src/tools/angular/compilation/aot-compilation.ts b/packages/angular/build/src/tools/angular/compilation/aot-compilation.ts index e4047a3e3e1b..46ee44048b8a 100644 --- a/packages/angular/build/src/tools/angular/compilation/aot-compilation.ts +++ b/packages/angular/build/src/tools/angular/compilation/aot-compilation.ts @@ -77,6 +77,7 @@ export class AotCompilation extends TypeScriptCompilation { rootNames, errors: configurationDiagnostics, warnings, + extendedConfigFiles, } = await this.loadConfiguration(tsconfig, compilerOptionOverrides, buildType); const useTypeScriptTranspilation = @@ -209,25 +210,28 @@ export class AotCompilation extends TypeScriptCompilation { const componentResourcesDependencies = new Map(); // Get all files referenced in the TypeScript/Angular program including component resources - const referencedFiles = typeScriptProgram - .getSourceFiles() - .filter((sourceFile) => !angularCompiler.ignoreForEmit.has(sourceFile)) - .flatMap((sourceFile) => { - const resourceDependencies = angularCompiler.getResourceDependencies(sourceFile); - componentResourcesDependencies.set(sourceFile.fileName, resourceDependencies); - // Also invalidate Angular diagnostics for a source file if component resources are modified - if (this.#state && hostOptions.modifiedFiles?.size) { - for (const resourceDependency of resourceDependencies) { - if (hostOptions.modifiedFiles.has(resourceDependency)) { - this.#state.diagnosticCache.delete(sourceFile); - // Also mark as affected in case changed template affects diagnostics - affectedFiles.add(sourceFile); + const referencedFiles = [ + ...(extendedConfigFiles ?? []), + ...typeScriptProgram + .getSourceFiles() + .filter((sourceFile) => !angularCompiler.ignoreForEmit.has(sourceFile)) + .flatMap((sourceFile) => { + const resourceDependencies = angularCompiler.getResourceDependencies(sourceFile); + componentResourcesDependencies.set(sourceFile.fileName, resourceDependencies); + // Also invalidate Angular diagnostics for a source file if component resources are modified + if (this.#state && hostOptions.modifiedFiles?.size) { + for (const resourceDependency of resourceDependencies) { + if (hostOptions.modifiedFiles.has(resourceDependency)) { + this.#state.diagnosticCache.delete(sourceFile); + // Also mark as affected in case changed template affects diagnostics + affectedFiles.add(sourceFile); + } } } - } - return [sourceFile.fileName, ...resourceDependencies]; - }); + return [sourceFile.fileName, ...resourceDependencies]; + }), + ]; this.#state = new AngularCompilationState( angularProgram, diff --git a/packages/angular/build/src/tools/angular/compilation/jit-compilation.ts b/packages/angular/build/src/tools/angular/compilation/jit-compilation.ts index 773279e236a9..fc63ee1d0faa 100644 --- a/packages/angular/build/src/tools/angular/compilation/jit-compilation.ts +++ b/packages/angular/build/src/tools/angular/compilation/jit-compilation.ts @@ -55,6 +55,7 @@ export class JitCompilation extends TypeScriptCompilation { rootNames, errors: configurationDiagnostics, warnings, + extendedConfigFiles, } = await this.loadConfiguration(tsconfig, compilerOptionOverrides, buildType); if (hostOptions.modifiedFiles) { @@ -89,9 +90,10 @@ export class JitCompilation extends TypeScriptCompilation { createWorkerTransformer(hostOptions.processWebWorker.bind(hostOptions)), ); - const referencedFiles = typeScriptProgram - .getSourceFiles() - .map((sourceFile) => sourceFile.fileName); + const referencedFiles = [ + ...(extendedConfigFiles ?? []), + ...typeScriptProgram.getSourceFiles().map((sourceFile) => sourceFile.fileName), + ]; return { compilerOptions, referencedFiles, warnings }; } diff --git a/packages/angular/build/src/tools/angular/compilation/typescript-compilation.ts b/packages/angular/build/src/tools/angular/compilation/typescript-compilation.ts index 2595bc60f55a..914c846c7fac 100644 --- a/packages/angular/build/src/tools/angular/compilation/typescript-compilation.ts +++ b/packages/angular/build/src/tools/angular/compilation/typescript-compilation.ts @@ -20,10 +20,14 @@ export interface TransformedConfiguration { rootNames: string[]; errors: ts.Diagnostic[]; warnings: PartialMessage[]; + extendedConfigFiles?: readonly string[]; } export abstract class TypeScriptCompilation extends AngularCompilation { static #angularCompilerCliModule?: typeof ng; + #cachedConfiguration?: TransformedConfiguration; + #cachedRootFiles?: readonly string[]; + readonly #extendedConfigCache = new Map(); static async loadCompilerCli(): Promise { TypeScriptCompilation.#angularCompilerCliModule ??= await import('@angular/compiler-cli'); @@ -36,29 +40,55 @@ export abstract class TypeScriptCompilation extends AngularCompilation { compilerOptionOverrides?: CompilerOptionOverrides, buildType: 'application' | 'library' = 'application', ): Promise { - const { readConfiguration } = await TypeScriptCompilation.loadCompilerCli(); + // When `rootFiles` are explicitly provided (e.g., library builder), avoid re-parsing `tsconfig.json` + // and walking the project directory tree via `readConfiguration` on every watch rebuild (~200-350ms on large libraries). + const currentRootFiles = compilerOptionOverrides?.rootFiles; + if ( + currentRootFiles && + this.#cachedConfiguration && + this.#cachedRootFiles && + this.#cachedRootFiles.length === currentRootFiles.length && + this.#cachedRootFiles.every((file, index) => file === currentRootFiles[index]) + ) { + return this.#cachedConfiguration; + } + + // TODO(alanagius): remove casting when @angular/compiler-cli exports the correct typings. + const { readConfiguration } = (await TypeScriptCompilation.loadCompilerCli()) as typeof ng & { + readConfiguration( + project: string, + existingOptions?: ng.CompilerOptions, + host?: unknown, + extendedConfigCache?: Map, + ): ng.ParsedConfiguration; + }; const { options: originalCompilerOptions, rootNames: originalRootNames, errors, } = profileSync('NG_READ_CONFIG', () => - readConfiguration(tsconfig, { - // Angular specific configuration defaults and overrides to ensure a functioning compilation. - suppressOutputPathCheck: true, - outDir: undefined, - sourceMap: false, - declaration: false, - declarationMap: false, - allowEmptyCodegenFiles: false, - annotationsAs: 'decorators', - enableResourceInlining: false, - supportTestBed: false, - supportJitMode: false, - // Disable removing of comments as TS is quite aggressive with these and can - // remove important annotations, such as /* @__PURE__ */ and comments like /* vite-ignore */. - removeComments: false, - }), + readConfiguration( + tsconfig, + { + // Angular specific configuration defaults and overrides to ensure a functioning compilation. + suppressOutputPathCheck: true, + outDir: undefined, + sourceMap: false, + declaration: false, + declarationMap: false, + allowEmptyCodegenFiles: false, + annotationsAs: 'decorators', + enableResourceInlining: false, + supportTestBed: false, + supportJitMode: false, + // Disable removing of comments as TS is quite aggressive with these and can + // remove important annotations, such as /* @__PURE__ */ and comments like /* vite-ignore */. + removeComments: false, + }, + undefined, + this.#extendedConfigCache, + ), ); let rootNames = originalRootNames; @@ -82,19 +112,53 @@ export abstract class TypeScriptCompilation extends AngularCompilation { buildType, ); - return { + const config: TransformedConfiguration = { compilerOptions, rootNames, errors, warnings, + extendedConfigFiles: [...this.#extendedConfigCache.keys()], }; + + if (config.extendedConfigFiles?.length && currentRootFiles?.length) { + this.#cachedConfiguration = config; + this.#cachedRootFiles = currentRootFiles; + } + + return config; } protected readonly sourceFiles = new Map(); protected invalidateFiles(files: Iterable): void { for (const file of files) { - this.sourceFiles.delete(toPosixPath(file)); + const posixFile = toPosixPath(file); + this.sourceFiles.delete(posixFile); + + if (!this.#extendedConfigCache.size) { + continue; + } + + let cacheKey: string | undefined; + if (this.#extendedConfigCache.has(posixFile)) { + cacheKey = posixFile; + } else { + // Check with lowercased key because TypeScript lowercases the keys + // of the extended config cache on case-insensitive operating systems. + const lowerCasedPosixFile = posixFile.toLowerCase(); + if (this.#extendedConfigCache.has(lowerCasedPosixFile)) { + cacheKey = lowerCasedPosixFile; + } + } + + if (!cacheKey) { + continue; + } + + // If a tsconfig changes, we need to re-read the configuration. + this.#cachedConfiguration = undefined; + this.#cachedRootFiles = undefined; + this.#extendedConfigCache.delete(cacheKey); } } From 542cd5c513664ec4a0e9b6580cd6a66917b7614c Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:04:39 +0000 Subject: [PATCH 2/5] fixup! perf(@angular/build): cache transformed configuration in TypeScript compilation --- .../src/tools/angular/compilation/angular-compilation_spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/angular/build/src/tools/angular/compilation/angular-compilation_spec.ts b/packages/angular/build/src/tools/angular/compilation/angular-compilation_spec.ts index 71eacd562259..077a296d0af3 100644 --- a/packages/angular/build/src/tools/angular/compilation/angular-compilation_spec.ts +++ b/packages/angular/build/src/tools/angular/compilation/angular-compilation_spec.ts @@ -158,6 +158,8 @@ describe('AngularCompilation', () => { suppressOutputPathCheck: true, outDir: undefined, }), + undefined, + jasmine.any(Map), ); expect(result.rootNames).toEqual(['/src/main.ts']); expect(result.compilerOptions.target).toBe(ts.ScriptTarget.ES2022); From 6c1e983ddf90a8d5f395ecf3a9d1a04c543e099e Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Wed, 30 Sep 2026 09:51:32 +0000 Subject: [PATCH 3/5] fixup! perf(@angular/build): cache transformed configuration in TypeScript compilation --- .../compilation/angular-compilation_spec.ts | 35 +++++++++++++++++++ .../angular/compilation/aot-compilation.ts | 4 +-- .../angular/compilation/jit-compilation.ts | 4 +-- .../compilation/typescript-compilation.ts | 27 ++++++++------ 4 files changed, 55 insertions(+), 15 deletions(-) diff --git a/packages/angular/build/src/tools/angular/compilation/angular-compilation_spec.ts b/packages/angular/build/src/tools/angular/compilation/angular-compilation_spec.ts index 077a296d0af3..25acd7cc07f2 100644 --- a/packages/angular/build/src/tools/angular/compilation/angular-compilation_spec.ts +++ b/packages/angular/build/src/tools/angular/compilation/angular-compilation_spec.ts @@ -165,6 +165,41 @@ describe('AngularCompilation', () => { expect(result.compilerOptions.target).toBe(ts.ScriptTarget.ES2022); expect(result.compilerOptions.inlineSources).toBe(true); expect(result.warnings.length).toBeGreaterThan(0); + expect(result.tsConfigFiles).toEqual(['tsconfig.json']); + }); + + it('caches transformed configuration when rootFiles are provided and invalidates on config change', async () => { + const compilation = new MockTypeScriptCompilation(); + const mockReadConfig = jasmine.createSpy('readConfiguration').and.returnValue({ + options: { target: ts.ScriptTarget.ES2020 }, + rootNames: ['/src/main.ts'], + errors: [], + }); + spyOn(TypeScriptCompilation, 'loadCompilerCli').and.resolveTo({ + readConfiguration: mockReadConfig, + } as unknown as typeof import('@angular/compiler-cli')); + + const overrides: CompilerOptionOverrides = { rootFiles: ['/src/main.ts'] }; + + const result1 = await compilation.testLoadConfiguration('tsconfig.json', overrides); + expect(mockReadConfig).toHaveBeenCalledTimes(1); + + // Re-loading with same rootFiles should return cached configuration without calling readConfiguration + const result2 = await compilation.testLoadConfiguration('tsconfig.json', overrides); + expect(mockReadConfig).toHaveBeenCalledTimes(1); + expect(result2).toBe(result1); + + // Invalidation of non-config file should keep cache intact + await compilation.update?.(new Set(['/src/main.ts'])); + const result3 = await compilation.testLoadConfiguration('tsconfig.json', overrides); + expect(mockReadConfig).toHaveBeenCalledTimes(1); + expect(result3).toBe(result1); + + // Invalidation of tsconfig file should clear cache + await compilation.update?.(new Set(['tsconfig.json'])); + const result4 = await compilation.testLoadConfiguration('tsconfig.json', overrides); + expect(mockReadConfig).toHaveBeenCalledTimes(2); + expect(result4).toBeDefined(); }); }); diff --git a/packages/angular/build/src/tools/angular/compilation/aot-compilation.ts b/packages/angular/build/src/tools/angular/compilation/aot-compilation.ts index 46ee44048b8a..c5b485344416 100644 --- a/packages/angular/build/src/tools/angular/compilation/aot-compilation.ts +++ b/packages/angular/build/src/tools/angular/compilation/aot-compilation.ts @@ -77,7 +77,7 @@ export class AotCompilation extends TypeScriptCompilation { rootNames, errors: configurationDiagnostics, warnings, - extendedConfigFiles, + tsConfigFiles, } = await this.loadConfiguration(tsconfig, compilerOptionOverrides, buildType); const useTypeScriptTranspilation = @@ -211,7 +211,7 @@ export class AotCompilation extends TypeScriptCompilation { // Get all files referenced in the TypeScript/Angular program including component resources const referencedFiles = [ - ...(extendedConfigFiles ?? []), + ...tsConfigFiles, ...typeScriptProgram .getSourceFiles() .filter((sourceFile) => !angularCompiler.ignoreForEmit.has(sourceFile)) diff --git a/packages/angular/build/src/tools/angular/compilation/jit-compilation.ts b/packages/angular/build/src/tools/angular/compilation/jit-compilation.ts index fc63ee1d0faa..ec88987369b0 100644 --- a/packages/angular/build/src/tools/angular/compilation/jit-compilation.ts +++ b/packages/angular/build/src/tools/angular/compilation/jit-compilation.ts @@ -55,7 +55,7 @@ export class JitCompilation extends TypeScriptCompilation { rootNames, errors: configurationDiagnostics, warnings, - extendedConfigFiles, + tsConfigFiles, } = await this.loadConfiguration(tsconfig, compilerOptionOverrides, buildType); if (hostOptions.modifiedFiles) { @@ -91,7 +91,7 @@ export class JitCompilation extends TypeScriptCompilation { ); const referencedFiles = [ - ...(extendedConfigFiles ?? []), + ...tsConfigFiles, ...typeScriptProgram.getSourceFiles().map((sourceFile) => sourceFile.fileName), ]; diff --git a/packages/angular/build/src/tools/angular/compilation/typescript-compilation.ts b/packages/angular/build/src/tools/angular/compilation/typescript-compilation.ts index 914c846c7fac..8a9a9d4b6a37 100644 --- a/packages/angular/build/src/tools/angular/compilation/typescript-compilation.ts +++ b/packages/angular/build/src/tools/angular/compilation/typescript-compilation.ts @@ -20,13 +20,14 @@ export interface TransformedConfiguration { rootNames: string[]; errors: ts.Diagnostic[]; warnings: PartialMessage[]; - extendedConfigFiles?: readonly string[]; + tsConfigFiles: readonly string[]; } export abstract class TypeScriptCompilation extends AngularCompilation { static #angularCompilerCliModule?: typeof ng; #cachedConfiguration?: TransformedConfiguration; #cachedRootFiles?: readonly string[]; + #tsConfigFiles?: Set; readonly #extendedConfigCache = new Map(); static async loadCompilerCli(): Promise { @@ -91,6 +92,8 @@ export abstract class TypeScriptCompilation extends AngularCompilation { ), ); + this.#tsConfigFiles = new Set([toPosixPath(tsconfig), ...this.#extendedConfigCache.keys()]); + let rootNames = originalRootNames; if (compilerOptionOverrides?.rootFiles?.length) { const rootFilesSet = new Set( @@ -117,10 +120,10 @@ export abstract class TypeScriptCompilation extends AngularCompilation { rootNames, errors, warnings, - extendedConfigFiles: [...this.#extendedConfigCache.keys()], + tsConfigFiles: Array.from(this.#tsConfigFiles), }; - if (config.extendedConfigFiles?.length && currentRootFiles?.length) { + if (currentRootFiles?.length) { this.#cachedConfiguration = config; this.#cachedRootFiles = currentRootFiles; } @@ -131,34 +134,36 @@ export abstract class TypeScriptCompilation extends AngularCompilation { protected readonly sourceFiles = new Map(); protected invalidateFiles(files: Iterable): void { + const tsConfigFiles = this.#tsConfigFiles; + for (const file of files) { const posixFile = toPosixPath(file); this.sourceFiles.delete(posixFile); - if (!this.#extendedConfigCache.size) { + if (!tsConfigFiles) { continue; } - let cacheKey: string | undefined; - if (this.#extendedConfigCache.has(posixFile)) { - cacheKey = posixFile; + let tsConfigCachedPath: string | undefined; + if (tsConfigFiles.has(posixFile)) { + tsConfigCachedPath = posixFile; } else { // Check with lowercased key because TypeScript lowercases the keys // of the extended config cache on case-insensitive operating systems. const lowerCasedPosixFile = posixFile.toLowerCase(); - if (this.#extendedConfigCache.has(lowerCasedPosixFile)) { - cacheKey = lowerCasedPosixFile; + if (tsConfigFiles.has(lowerCasedPosixFile)) { + tsConfigCachedPath = lowerCasedPosixFile; } } - if (!cacheKey) { + if (!tsConfigCachedPath) { continue; } // If a tsconfig changes, we need to re-read the configuration. this.#cachedConfiguration = undefined; this.#cachedRootFiles = undefined; - this.#extendedConfigCache.delete(cacheKey); + this.#extendedConfigCache.delete(tsConfigCachedPath); } } From 3428fa2bfd37b23b56eddf1cc707171a3fd604b6 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Wed, 30 Sep 2026 13:43:19 +0000 Subject: [PATCH 4/5] fixup! perf(@angular/build): cache transformed configuration in TypeScript compilation --- .../src/tools/angular/compilation/typescript-compilation.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/angular/build/src/tools/angular/compilation/typescript-compilation.ts b/packages/angular/build/src/tools/angular/compilation/typescript-compilation.ts index 8a9a9d4b6a37..504a3091a569 100644 --- a/packages/angular/build/src/tools/angular/compilation/typescript-compilation.ts +++ b/packages/angular/build/src/tools/angular/compilation/typescript-compilation.ts @@ -92,7 +92,8 @@ export abstract class TypeScriptCompilation extends AngularCompilation { ), ); - this.#tsConfigFiles = new Set([toPosixPath(tsconfig), ...this.#extendedConfigCache.keys()]); + const tsConfigFiles = [toPosixPath(tsconfig), ...this.#extendedConfigCache.keys()]; + this.#tsConfigFiles = new Set(tsConfigFiles); let rootNames = originalRootNames; if (compilerOptionOverrides?.rootFiles?.length) { @@ -120,7 +121,7 @@ export abstract class TypeScriptCompilation extends AngularCompilation { rootNames, errors, warnings, - tsConfigFiles: Array.from(this.#tsConfigFiles), + tsConfigFiles, }; if (currentRootFiles?.length) { From 168aa512ec53897b1a5519252038ee09e2df76ce Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Thu, 1 Oct 2026 09:44:16 +0000 Subject: [PATCH 5/5] fixup! perf(@angular/build): cache transformed configuration in TypeScript compilation --- .../angular/compilation/typescript-compilation.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/angular/build/src/tools/angular/compilation/typescript-compilation.ts b/packages/angular/build/src/tools/angular/compilation/typescript-compilation.ts index 504a3091a569..86e6237ad8b7 100644 --- a/packages/angular/build/src/tools/angular/compilation/typescript-compilation.ts +++ b/packages/angular/build/src/tools/angular/compilation/typescript-compilation.ts @@ -54,15 +54,7 @@ export abstract class TypeScriptCompilation extends AngularCompilation { return this.#cachedConfiguration; } - // TODO(alanagius): remove casting when @angular/compiler-cli exports the correct typings. - const { readConfiguration } = (await TypeScriptCompilation.loadCompilerCli()) as typeof ng & { - readConfiguration( - project: string, - existingOptions?: ng.CompilerOptions, - host?: unknown, - extendedConfigCache?: Map, - ): ng.ParsedConfiguration; - }; + const { readConfiguration } = await TypeScriptCompilation.loadCompilerCli(); const { options: originalCompilerOptions,