Skip to content

Commit 9eb7fb5

Browse files
committed
fix(@ngtools/webpack): reduce overhead of Angular compiler rebuild requests
This change adds additional checks to reduce the number of Webpack `rebuildModule` calls when the Angular compiler requests additional files to be rebuilt. Now if an emitted file's output does not change from its previous emit, a Webpack rebuild of the module is not performed. This can greatly reduce the amount of computation needed during a rebuild as any files that required re-analysis by the Angular compiler but whose final output did not change will not trigger potential expensive Webpack module graph analysis and additonal module rebuilds.
1 parent d1f6169 commit 9eb7fb5

File tree

2 files changed

+72
-26
lines changed

2 files changed

+72
-26
lines changed

packages/ngtools/webpack/src/ivy/plugin.ts

+71-26
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88
import { CompilerHost, CompilerOptions, readConfiguration } from '@angular/compiler-cli';
99
import { NgtscProgram } from '@angular/compiler-cli/src/ngtsc/program';
10+
import { createHash } from 'crypto';
1011
import * as path from 'path';
1112
import * as ts from 'typescript';
1213
import {
@@ -31,7 +32,7 @@ import {
3132
augmentProgramWithVersioning,
3233
} from './host';
3334
import { externalizePath, normalizePath } from './paths';
34-
import { AngularPluginSymbol, FileEmitter } from './symbol';
35+
import { AngularPluginSymbol, EmitFileResult, FileEmitter } from './symbol';
3536
import { createWebpackSystem } from './system';
3637
import { createAotTransformers, createJitTransformers, mergeTransformers } from './transformation';
3738

@@ -76,6 +77,10 @@ function initializeNgccProcessor(
7677
return { processor, errors, warnings };
7778
}
7879

80+
function hashContent(content: string): Uint8Array {
81+
return createHash('md5').update(content).digest();
82+
}
83+
7984
const PLUGIN_NAME = 'angular-compiler';
8085

8186
export class AngularWebpackPlugin {
@@ -87,6 +92,8 @@ export class AngularWebpackPlugin {
8792
private buildTimestamp!: number;
8893
private readonly lazyRouteMap: Record<string, string> = {};
8994
private readonly requiredFilesToEmit = new Set<string>();
95+
private readonly requiredFilesToEmitCache = new Map<string, EmitFileResult | undefined>();
96+
private readonly fileEmitHistory = new Map<string, { length: number; hash: Uint8Array }>();
9097

9198
constructor(options: Partial<AngularPluginOptions> = {}) {
9299
this.pluginOptions = {
@@ -181,10 +188,7 @@ export class AngularWebpackPlugin {
181188
pathsPlugin.update(compilerOptions);
182189

183190
// Create a Webpack-based TypeScript compiler host
184-
const system = createWebpackSystem(
185-
compiler.inputFileSystem,
186-
normalizePath(compiler.context),
187-
);
191+
const system = createWebpackSystem(compiler.inputFileSystem, normalizePath(compiler.context));
188192
const host = ts.createIncrementalCompilerHost(compilerOptions, system);
189193

190194
// Setup source file caching and reuse cache from previous compilation if present
@@ -251,22 +255,7 @@ export class AngularWebpackPlugin {
251255

252256
compilation.hooks.finishModules.tapPromise(PLUGIN_NAME, async (modules) => {
253257
// Rebuild any remaining AOT required modules
254-
const rebuild = (filename: string) => new Promise<void>((resolve) => {
255-
const module = modules.find(
256-
({ resource }: compilation.Module & { resource?: string }) =>
257-
resource && normalizePath(resource) === filename,
258-
);
259-
if (!module) {
260-
resolve();
261-
} else {
262-
compilation.rebuildModule(module, resolve);
263-
}
264-
});
265-
266-
for (const requiredFile of this.requiredFilesToEmit) {
267-
await rebuild(requiredFile);
268-
}
269-
this.requiredFilesToEmit.clear();
258+
await this.rebuildRequiredFiles(modules, compilation, fileEmitter);
270259

271260
// Analyze program for unused files
272261
if (compilation.errors.length > 0) {
@@ -304,6 +293,47 @@ export class AngularWebpackPlugin {
304293
});
305294
}
306295

296+
private async rebuildRequiredFiles(
297+
modules: compilation.Module[],
298+
compilation: WebpackCompilation,
299+
fileEmitter: FileEmitter,
300+
) {
301+
const rebuild = (filename: string) =>
302+
new Promise<void>((resolve) => {
303+
const module = modules.find(
304+
({ resource }: compilation.Module & { resource?: string }) =>
305+
resource && normalizePath(resource) === filename,
306+
);
307+
if (!module) {
308+
resolve();
309+
} else {
310+
compilation.rebuildModule(module, resolve);
311+
}
312+
});
313+
314+
for (const requiredFile of this.requiredFilesToEmit) {
315+
const history = this.fileEmitHistory.get(requiredFile);
316+
if (history) {
317+
const emitResult = await fileEmitter(requiredFile);
318+
if (
319+
emitResult?.content === undefined ||
320+
history.length !== emitResult.content.length ||
321+
emitResult.hash === undefined ||
322+
Buffer.compare(history.hash, emitResult.hash) !== 0
323+
) {
324+
// New emit result is different so rebuild using new emit result
325+
this.requiredFilesToEmitCache.set(requiredFile, emitResult);
326+
await rebuild(requiredFile);
327+
}
328+
} else {
329+
// No emit history so rebuild
330+
await rebuild(requiredFile);
331+
}
332+
}
333+
this.requiredFilesToEmit.clear();
334+
this.requiredFilesToEmitCache.clear();
335+
}
336+
307337
private loadConfiguration(compilation: WebpackCompilation) {
308338
const { options: compilerOptions, rootNames, errors } = readConfiguration(
309339
this.pluginOptions.tsconfig,
@@ -432,10 +462,14 @@ export class AngularWebpackPlugin {
432462
if (angularCompiler.getDiagnosticsForFile) {
433463
// @angular/compiler-cli 11.1+
434464
const { OptimizeFor } = require('@angular/compiler-cli/src/ngtsc/typecheck/api');
435-
diagnosticsReporter(angularCompiler.getDiagnosticsForFile(sourceFile, OptimizeFor.WholeProgram));
465+
diagnosticsReporter(
466+
angularCompiler.getDiagnosticsForFile(sourceFile, OptimizeFor.WholeProgram),
467+
);
436468
} else {
437469
// @angular/compiler-cli 11.0+
438-
const getDiagnostics = angularCompiler.getDiagnostics as (sourceFile: ts.SourceFile) => ts.Diagnostic[];
470+
const getDiagnostics = angularCompiler.getDiagnostics as (
471+
sourceFile: ts.SourceFile,
472+
) => ts.Diagnostic[];
439473
diagnosticsReporter(getDiagnostics.call(angularCompiler, sourceFile));
440474
}
441475
}
@@ -550,13 +584,17 @@ export class AngularWebpackPlugin {
550584
onAfterEmit?: (sourceFile: ts.SourceFile) => void,
551585
): FileEmitter {
552586
return async (file: string) => {
587+
if (this.requiredFilesToEmitCache.has(file)) {
588+
return this.requiredFilesToEmitCache.get(file);
589+
}
590+
553591
const sourceFile = program.getSourceFile(file);
554592
if (!sourceFile) {
555593
return undefined;
556594
}
557595

558-
let content: string | undefined = undefined;
559-
let map: string | undefined = undefined;
596+
let content: string | undefined;
597+
let map: string | undefined;
560598
program.emit(
561599
sourceFile,
562600
(filename, data) => {
@@ -573,12 +611,19 @@ export class AngularWebpackPlugin {
573611

574612
onAfterEmit?.(sourceFile);
575613

614+
let hash;
615+
if (content !== undefined && this.watchMode) {
616+
// Capture emit history info for Angular rebuild analysis
617+
hash = hashContent(content);
618+
this.fileEmitHistory.set(file, { length: content.length, hash });
619+
}
620+
576621
const dependencies = [
577622
...program.getAllDependencies(sourceFile),
578623
...getExtraDependencies(sourceFile),
579624
].map(externalizePath);
580625

581-
return { content, map, dependencies };
626+
return { content, map, dependencies, hash };
582627
};
583628
}
584629
}

packages/ngtools/webpack/src/ivy/symbol.ts

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export interface EmitFileResult {
1111
content?: string;
1212
map?: string;
1313
dependencies: readonly string[];
14+
hash?: Uint8Array;
1415
}
1516

1617
export type FileEmitter = (file: string) => Promise<EmitFileResult | undefined>;

0 commit comments

Comments
 (0)