perf(compiler-cli): only emit directive/pipe references that are used (#38539)

For the compilation of a component, the compiler has to prepare some
information about the directives and pipes that are used in the template.
This information includes an expression for directives/pipes, for usage
within the compilation output. For large NgModule compilation scopes
this has shown to introduce a performance hotspot, as the generation of
expressions is quite expensive. This commit reduces the performance
overhead by only generating expressions for the directives/pipes that
are actually used within the template, significantly cutting down on
the compiler's resolve phase.

PR Close #38539
This commit is contained in:
JoostK 2020-08-18 21:40:35 +02:00 committed by atscott
parent 4360eed9b7
commit 4faac78e32
1 changed files with 32 additions and 24 deletions

View File

@ -495,36 +495,49 @@ export class ComponentDecoratorHandler implements
// Set up the R3TargetBinder, as well as a 'directives' array and a 'pipes' map that are later // Set up the R3TargetBinder, as well as a 'directives' array and a 'pipes' map that are later
// fed to the TemplateDefinitionBuilder. First, a SelectorMatcher is constructed to match // fed to the TemplateDefinitionBuilder. First, a SelectorMatcher is constructed to match
// directives that are in scope. // directives that are in scope.
const matcher = new SelectorMatcher<DirectiveMeta&{expression: Expression}>(); type MatchedDirective = DirectiveMeta&{selector: string};
const directives: {selector: string, expression: Expression}[] = []; const matcher = new SelectorMatcher<MatchedDirective>();
for (const dir of scope.compilation.directives) { for (const dir of scope.compilation.directives) {
const {ref, selector} = dir; if (dir.selector !== null) {
if (selector !== null) { matcher.addSelectables(CssSelector.parse(dir.selector), dir as MatchedDirective);
const expression = this.refEmitter.emit(ref, context);
directives.push({selector, expression});
matcher.addSelectables(CssSelector.parse(selector), {...dir, expression});
} }
} }
const pipes = new Map<string, Expression>(); const pipes = new Map<string, Reference<ClassDeclaration>>();
for (const pipe of scope.compilation.pipes) { for (const pipe of scope.compilation.pipes) {
pipes.set(pipe.name, this.refEmitter.emit(pipe.ref, context)); pipes.set(pipe.name, pipe.ref);
} }
// Next, the component template AST is bound using the R3TargetBinder. This produces an // Next, the component template AST is bound using the R3TargetBinder. This produces a
// BoundTarget, which is similar to a ts.TypeChecker. // BoundTarget, which is similar to a ts.TypeChecker.
const binder = new R3TargetBinder(matcher); const binder = new R3TargetBinder(matcher);
const bound = binder.bind({template: metadata.template.nodes}); const bound = binder.bind({template: metadata.template.nodes});
// The BoundTarget knows which directives and pipes matched the template. // The BoundTarget knows which directives and pipes matched the template.
const usedDirectives = bound.getUsedDirectives(); const usedDirectives = bound.getUsedDirectives().map(directive => {
const usedPipes = bound.getUsedPipes().map(name => pipes.get(name)!); return {
selector: directive.selector,
expression: this.refEmitter.emit(directive.ref, context),
};
});
const usedPipes: {pipeName: string, expression: Expression}[] = [];
for (const pipeName of bound.getUsedPipes()) {
if (!pipes.has(pipeName)) {
continue;
}
const pipe = pipes.get(pipeName)!;
usedPipes.push({
pipeName,
expression: this.refEmitter.emit(pipe, context),
});
}
// Scan through the directives/pipes actually used in the template and check whether any // Scan through the directives/pipes actually used in the template and check whether any
// import which needs to be generated would create a cycle. // import which needs to be generated would create a cycle.
const cycleDetected = const cycleDetected =
usedDirectives.some(dir => this._isCyclicImport(dir.expression, context)) || usedDirectives.some(dir => this._isCyclicImport(dir.expression, context)) ||
usedPipes.some(pipe => this._isCyclicImport(pipe, context)); usedPipes.some(pipe => this._isCyclicImport(pipe.expression, context));
if (!cycleDetected) { if (!cycleDetected) {
// No cycle was detected. Record the imports that need to be created in the cycle detector // No cycle was detected. Record the imports that need to be created in the cycle detector
@ -532,8 +545,8 @@ export class ComponentDecoratorHandler implements
for (const {expression} of usedDirectives) { for (const {expression} of usedDirectives) {
this._recordSyntheticImport(expression, context); this._recordSyntheticImport(expression, context);
} }
for (const pipe of usedPipes) { for (const {expression} of usedPipes) {
this._recordSyntheticImport(pipe, context); this._recordSyntheticImport(expression, context);
} }
// Check whether the directive/pipe arrays in ɵcmp need to be wrapped in closures. // Check whether the directive/pipe arrays in ɵcmp need to be wrapped in closures.
@ -542,16 +555,11 @@ export class ComponentDecoratorHandler implements
const wrapDirectivesAndPipesInClosure = const wrapDirectivesAndPipesInClosure =
usedDirectives.some( usedDirectives.some(
dir => isExpressionForwardReference(dir.expression, node.name, context)) || dir => isExpressionForwardReference(dir.expression, node.name, context)) ||
usedPipes.some(pipe => isExpressionForwardReference(pipe, node.name, context)); usedPipes.some(
pipe => isExpressionForwardReference(pipe.expression, node.name, context));
// Actual compilation still uses the full scope, not the narrowed scope determined by data.directives = usedDirectives;
// R3TargetBinder. This is a hedge against potential issues with the R3TargetBinder - right data.pipes = new Map(usedPipes.map(pipe => [pipe.pipeName, pipe.expression]));
// now the TemplateDefinitionBuilder is the "source of truth" for which directives/pipes are
// actually used (though the two should agree perfectly).
//
// TODO(alxhub): switch TemplateDefinitionBuilder over to using R3TargetBinder directly.
data.directives = directives;
data.pipes = pipes;
data.wrapDirectivesAndPipesInClosure = wrapDirectivesAndPipesInClosure; data.wrapDirectivesAndPipesInClosure = wrapDirectivesAndPipesInClosure;
} else { } else {
// Declaring the directiveDefs/pipeDefs arrays directly would require imports that would // Declaring the directiveDefs/pipeDefs arrays directly would require imports that would