Skip to content

Commit 440fe64

Browse files
committed
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.
1 parent 36bdd84 commit 440fe64

3 files changed

Lines changed: 108 additions & 38 deletions

File tree

‎packages/angular/build/src/tools/angular/compilation/aot-compilation.ts‎

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ export class AotCompilation extends TypeScriptCompilation {
7777
rootNames,
7878
errors: configurationDiagnostics,
7979
warnings,
80+
extendedConfigFiles,
8081
} = await this.loadConfiguration(tsconfig, compilerOptionOverrides, buildType);
8182

8283
const useTypeScriptTranspilation =
@@ -209,25 +210,28 @@ export class AotCompilation extends TypeScriptCompilation {
209210
const componentResourcesDependencies = new Map<string, string[]>();
210211

211212
// Get all files referenced in the TypeScript/Angular program including component resources
212-
const referencedFiles = typeScriptProgram
213-
.getSourceFiles()
214-
.filter((sourceFile) => !angularCompiler.ignoreForEmit.has(sourceFile))
215-
.flatMap((sourceFile) => {
216-
const resourceDependencies = angularCompiler.getResourceDependencies(sourceFile);
217-
componentResourcesDependencies.set(sourceFile.fileName, resourceDependencies);
218-
// Also invalidate Angular diagnostics for a source file if component resources are modified
219-
if (this.#state && hostOptions.modifiedFiles?.size) {
220-
for (const resourceDependency of resourceDependencies) {
221-
if (hostOptions.modifiedFiles.has(resourceDependency)) {
222-
this.#state.diagnosticCache.delete(sourceFile);
223-
// Also mark as affected in case changed template affects diagnostics
224-
affectedFiles.add(sourceFile);
213+
const referencedFiles = [
214+
...(extendedConfigFiles ?? []),
215+
...typeScriptProgram
216+
.getSourceFiles()
217+
.filter((sourceFile) => !angularCompiler.ignoreForEmit.has(sourceFile))
218+
.flatMap((sourceFile) => {
219+
const resourceDependencies = angularCompiler.getResourceDependencies(sourceFile);
220+
componentResourcesDependencies.set(sourceFile.fileName, resourceDependencies);
221+
// Also invalidate Angular diagnostics for a source file if component resources are modified
222+
if (this.#state && hostOptions.modifiedFiles?.size) {
223+
for (const resourceDependency of resourceDependencies) {
224+
if (hostOptions.modifiedFiles.has(resourceDependency)) {
225+
this.#state.diagnosticCache.delete(sourceFile);
226+
// Also mark as affected in case changed template affects diagnostics
227+
affectedFiles.add(sourceFile);
228+
}
225229
}
226230
}
227-
}
228231

229-
return [sourceFile.fileName, ...resourceDependencies];
230-
});
232+
return [sourceFile.fileName, ...resourceDependencies];
233+
}),
234+
];
231235

232236
this.#state = new AngularCompilationState(
233237
angularProgram,

‎packages/angular/build/src/tools/angular/compilation/jit-compilation.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export class JitCompilation extends TypeScriptCompilation {
5555
rootNames,
5656
errors: configurationDiagnostics,
5757
warnings,
58+
extendedConfigFiles,
5859
} = await this.loadConfiguration(tsconfig, compilerOptionOverrides, buildType);
5960

6061
if (hostOptions.modifiedFiles) {
@@ -89,9 +90,10 @@ export class JitCompilation extends TypeScriptCompilation {
8990
createWorkerTransformer(hostOptions.processWebWorker.bind(hostOptions)),
9091
);
9192

92-
const referencedFiles = typeScriptProgram
93-
.getSourceFiles()
94-
.map((sourceFile) => sourceFile.fileName);
93+
const referencedFiles = [
94+
...(extendedConfigFiles ?? []),
95+
...typeScriptProgram.getSourceFiles().map((sourceFile) => sourceFile.fileName),
96+
];
9597

9698
return { compilerOptions, referencedFiles, warnings };
9799
}

‎packages/angular/build/src/tools/angular/compilation/typescript-compilation.ts‎

Lines changed: 83 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,14 @@ export interface TransformedConfiguration {
2020
rootNames: string[];
2121
errors: ts.Diagnostic[];
2222
warnings: PartialMessage[];
23+
extendedConfigFiles?: readonly string[];
2324
}
2425

2526
export abstract class TypeScriptCompilation extends AngularCompilation {
2627
static #angularCompilerCliModule?: typeof ng;
28+
#cachedConfiguration?: TransformedConfiguration;
29+
#cachedRootFiles?: readonly string[];
30+
readonly #extendedConfigCache = new Map<string, ts.ExtendedConfigCacheEntry>();
2731

2832
static async loadCompilerCli(): Promise<typeof ng> {
2933
TypeScriptCompilation.#angularCompilerCliModule ??= await import('@angular/compiler-cli');
@@ -36,29 +40,55 @@ export abstract class TypeScriptCompilation extends AngularCompilation {
3640
compilerOptionOverrides?: CompilerOptionOverrides,
3741
buildType: 'application' | 'library' = 'application',
3842
): Promise<TransformedConfiguration> {
39-
const { readConfiguration } = await TypeScriptCompilation.loadCompilerCli();
43+
// When `rootFiles` are explicitly provided (e.g., library builder), avoid re-parsing `tsconfig.json`
44+
// and walking the project directory tree via `readConfiguration` on every watch rebuild (~200-350ms on large libraries).
45+
const currentRootFiles = compilerOptionOverrides?.rootFiles;
46+
if (
47+
currentRootFiles &&
48+
this.#cachedConfiguration &&
49+
this.#cachedRootFiles &&
50+
this.#cachedRootFiles.length === currentRootFiles.length &&
51+
this.#cachedRootFiles.every((file, index) => file === currentRootFiles[index])
52+
) {
53+
return this.#cachedConfiguration;
54+
}
55+
56+
// TODO(alanagius): remove casting when @angular/compiler-cli exports the correct typings.
57+
const { readConfiguration } = (await TypeScriptCompilation.loadCompilerCli()) as typeof ng & {
58+
readConfiguration(
59+
project: string,
60+
existingOptions?: ng.CompilerOptions,
61+
host?: unknown,
62+
extendedConfigCache?: Map<string, ts.ExtendedConfigCacheEntry>,
63+
): ng.ParsedConfiguration;
64+
};
4065

4166
const {
4267
options: originalCompilerOptions,
4368
rootNames: originalRootNames,
4469
errors,
4570
} = profileSync('NG_READ_CONFIG', () =>
46-
readConfiguration(tsconfig, {
47-
// Angular specific configuration defaults and overrides to ensure a functioning compilation.
48-
suppressOutputPathCheck: true,
49-
outDir: undefined,
50-
sourceMap: false,
51-
declaration: false,
52-
declarationMap: false,
53-
allowEmptyCodegenFiles: false,
54-
annotationsAs: 'decorators',
55-
enableResourceInlining: false,
56-
supportTestBed: false,
57-
supportJitMode: false,
58-
// Disable removing of comments as TS is quite aggressive with these and can
59-
// remove important annotations, such as /* @__PURE__ */ and comments like /* vite-ignore */.
60-
removeComments: false,
61-
}),
71+
readConfiguration(
72+
tsconfig,
73+
{
74+
// Angular specific configuration defaults and overrides to ensure a functioning compilation.
75+
suppressOutputPathCheck: true,
76+
outDir: undefined,
77+
sourceMap: false,
78+
declaration: false,
79+
declarationMap: false,
80+
allowEmptyCodegenFiles: false,
81+
annotationsAs: 'decorators',
82+
enableResourceInlining: false,
83+
supportTestBed: false,
84+
supportJitMode: false,
85+
// Disable removing of comments as TS is quite aggressive with these and can
86+
// remove important annotations, such as /* @__PURE__ */ and comments like /* vite-ignore */.
87+
removeComments: false,
88+
},
89+
undefined,
90+
this.#extendedConfigCache,
91+
),
6292
);
6393

6494
let rootNames = originalRootNames;
@@ -82,19 +112,53 @@ export abstract class TypeScriptCompilation extends AngularCompilation {
82112
buildType,
83113
);
84114

85-
return {
115+
const config: TransformedConfiguration = {
86116
compilerOptions,
87117
rootNames,
88118
errors,
89119
warnings,
120+
extendedConfigFiles: [...this.#extendedConfigCache.keys()],
90121
};
122+
123+
if (config.extendedConfigFiles?.length && currentRootFiles?.length) {
124+
this.#cachedConfiguration = config;
125+
this.#cachedRootFiles = currentRootFiles;
126+
}
127+
128+
return config;
91129
}
92130

93131
protected readonly sourceFiles = new Map<string, ts.SourceFile>();
94132

95133
protected invalidateFiles(files: Iterable<string>): void {
96134
for (const file of files) {
97-
this.sourceFiles.delete(toPosixPath(file));
135+
const posixFile = toPosixPath(file);
136+
this.sourceFiles.delete(posixFile);
137+
138+
if (!this.#extendedConfigCache.size) {
139+
continue;
140+
}
141+
142+
let cacheKey: string | undefined;
143+
if (this.#extendedConfigCache.has(posixFile)) {
144+
cacheKey = posixFile;
145+
} else {
146+
// Check with lowercased key because TypeScript lowercases the keys
147+
// of the extended config cache on case-insensitive operating systems.
148+
const lowerCasedPosixFile = posixFile.toLowerCase();
149+
if (this.#extendedConfigCache.has(lowerCasedPosixFile)) {
150+
cacheKey = lowerCasedPosixFile;
151+
}
152+
}
153+
154+
if (!cacheKey) {
155+
continue;
156+
}
157+
158+
// If a tsconfig changes, we need to re-read the configuration.
159+
this.#cachedConfiguration = undefined;
160+
this.#cachedRootFiles = undefined;
161+
this.#extendedConfigCache.delete(cacheKey);
98162
}
99163
}
100164

0 commit comments

Comments
 (0)