Permalink
Browse files

perf(core): add option to remove blank text nodes from compiled templ…

…ates (#18823)

PR Close #18823
  • Loading branch information...
pkozlowski-opensource authored and mhevery committed Jul 28, 2017
1 parent 7ec28fe commit b8b551cf2b2d54cf78094bb2fbb35c19be40190d
Showing with 536 additions and 48 deletions.
  1. +1 −0 packages/compiler-cli/src/codegen.ts
  2. +4 −0 packages/compiler-cli/src/transformers/api.ts
  3. +1 −1 packages/compiler-cli/src/transformers/program.ts
  4. +2 −1 packages/compiler/src/aot/compiler.ts
  5. +1 −0 packages/compiler/src/aot/compiler_factory.ts
  6. +1 −0 packages/compiler/src/aot/compiler_options.ts
  7. +7 −3 packages/compiler/src/compile_metadata.ts
  8. +1 −1 packages/compiler/src/compiler.ts
  9. +11 −6 packages/compiler/src/config.ts
  10. +13 −2 packages/compiler/src/directive_normalizer.ts
  11. +2 −1 packages/compiler/src/directive_resolver.ts
  12. +3 −1 packages/compiler/src/jit/compiler.ts
  13. +3 −0 packages/compiler/src/jit/compiler_factory.ts
  14. +4 −2 packages/compiler/src/metadata_resolver.ts
  15. +86 −0 packages/compiler/src/ml_parser/html_whitespaces.ts
  16. +7 −0 packages/compiler/src/ml_parser/tags.ts
  17. +18 −10 packages/compiler/src/template_parser/template_parser.ts
  18. +48 −6 packages/compiler/test/directive_normalizer_spec.ts
  19. +7 −1 packages/compiler/test/directive_resolver_spec.ts
  20. +0 −1 packages/compiler/test/integration_spec.ts
  21. +1 −1 packages/compiler/test/ml_parser/ast_serializer_spec.ts
  22. +118 −0 packages/compiler/test/ml_parser/html_whitespaces_spec.ts
  23. +76 −10 packages/compiler/test/template_parser/template_parser_spec.ts
  24. +2 −1 packages/compiler/testing/src/directive_resolver_mock.ts
  25. +1 −0 packages/core/src/linker/compiler.ts
  26. +68 −0 packages/core/src/metadata/directives.ts
  27. +45 −0 packages/core/test/linker/integration_spec.ts
  28. +4 −0 tools/@angular/tsc-wrapped/src/options.ts
  29. +1 −0 tools/public_api_guard/core/core.d.ts
@@ -104,6 +104,7 @@ export class CodeGenerator {
locale: cliOptions.locale, missingTranslation,
enableLegacyTemplate: options.enableLegacyTemplate !== false,
enableSummariesForJit: options.enableSummariesForJit !== false,
preserveWhitespaces: options.preserveWhitespaces,
});
return new CodeGenerator(options, program, tsCompilerHost, aotCompiler, ngCompilerHost);
}
@@ -91,6 +91,10 @@ export interface CompilerOptions extends ts.CompilerOptions {
// Whether to enable support for <template> and the template attribute (true by default)
enableLegacyTemplate?: boolean;
// Whether to remove blank text nodes from compiled templates. It is `true` by default
// in Angular 4 and will be re-visited post Angular 5.
preserveWhitespaces?: boolean;
}
export interface ModuleFilenameResolver {
@@ -388,4 +388,4 @@ function createProgramWithStubsHost(
fileExists = (fileName: string) =>
this.generatedFiles.has(fileName) || originalHost.fileExists(fileName);
};
}
}
@@ -323,9 +323,10 @@ export class AotCompiler {
const pipes = ngModule.transitiveModule.pipes.map(
pipe => this._metadataResolver.getPipeSummary(pipe.reference));
const preserveWhitespaces = compMeta !.template !.preserveWhitespaces;
const {template: parsedTemplate, pipes: usedPipes} = this._templateParser.parse(
compMeta, compMeta.template !.template !, directives, pipes, ngModule.schemas,
templateSourceUrl(ngModule.type, compMeta, compMeta.template !));
templateSourceUrl(ngModule.type, compMeta, compMeta.template !), preserveWhitespaces);
const stylesExpr = componentStyles ? o.variable(componentStyles.stylesVar) : o.literalArr([]);
const viewResult = this._viewCompiler.compileComponent(
outputCtx, compMeta, parsedTemplate, stylesExpr, usedPipes);
@@ -54,6 +54,7 @@ export function createAotCompiler(compilerHost: AotCompilerHost, options: AotCom
useJit: false,
enableLegacyTemplate: options.enableLegacyTemplate !== false,
missingTranslation: options.missingTranslation,
preserveWhitespaces: options.preserveWhitespaces,
});
const normalizer = new DirectiveNormalizer(
{get: (url: string) => compilerHost.loadResource(url)}, urlResolver, htmlParser, config);
@@ -15,4 +15,5 @@ export interface AotCompilerOptions {
missingTranslation?: MissingTranslationStrategy;
enableLegacyTemplate?: boolean;
enableSummariesForJit?: boolean;
preserveWhitespaces?: boolean;
}
@@ -252,8 +252,9 @@ export class CompileTemplateMetadata {
animations: any[];
ngContentSelectors: string[];
interpolation: [string, string]|null;
preserveWhitespaces: boolean;
constructor({encapsulation, template, templateUrl, styles, styleUrls, externalStylesheets,
animations, ngContentSelectors, interpolation, isInline}: {
animations, ngContentSelectors, interpolation, isInline, preserveWhitespaces}: {
encapsulation: ViewEncapsulation | null,
template: string|null,
templateUrl: string|null,
@@ -263,7 +264,8 @@ export class CompileTemplateMetadata {
ngContentSelectors: string[],
animations: any[],
interpolation: [string, string]|null,
isInline: boolean
isInline: boolean,
preserveWhitespaces: boolean
}) {
this.encapsulation = encapsulation;
this.template = template;
@@ -278,6 +280,7 @@ export class CompileTemplateMetadata {
}
this.interpolation = interpolation;
this.isInline = isInline;
this.preserveWhitespaces = preserveWhitespaces;
}
toSummary(): CompileTemplateSummary {
@@ -516,7 +519,8 @@ export function createHostComponentMeta(
animations: [],
isInline: true,
externalStylesheets: [],
interpolation: null
interpolation: null,
preserveWhitespaces: false,
}),
exportAs: null,
changeDetection: ChangeDetectionStrategy.Default,
@@ -24,7 +24,7 @@
export {VERSION} from './version';
export * from './template_parser/template_ast';
export {TEMPLATE_TRANSFORMS} from './template_parser/template_parser';
export {CompilerConfig} from './config';
export {CompilerConfig, preserveWhitespacesDefault} from './config';
export * from './compile_metadata';
export * from './aot/compiler_factory';
export * from './aot/compiler';
@@ -6,11 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/
import {InjectionToken, MissingTranslationStrategy, ViewEncapsulation, isDevMode} from '@angular/core';
import {CompileIdentifierMetadata} from './compile_metadata';
import {Identifiers} from './identifiers';
import {MissingTranslationStrategy, ViewEncapsulation} from '@angular/core';
import {noUndefined} from './util';
export class CompilerConfig {
public defaultEncapsulation: ViewEncapsulation|null;
@@ -19,18 +16,26 @@ export class CompilerConfig {
public enableLegacyTemplate: boolean;
public useJit: boolean;
public missingTranslation: MissingTranslationStrategy|null;
public preserveWhitespaces: boolean;
constructor(
{defaultEncapsulation = ViewEncapsulation.Emulated, useJit = true, missingTranslation,
enableLegacyTemplate}: {
enableLegacyTemplate, preserveWhitespaces}: {
defaultEncapsulation?: ViewEncapsulation,
useJit?: boolean,
missingTranslation?: MissingTranslationStrategy,
enableLegacyTemplate?: boolean,
preserveWhitespaces?: boolean
} = {}) {
this.defaultEncapsulation = defaultEncapsulation;
this.useJit = !!useJit;
this.missingTranslation = missingTranslation || null;
this.enableLegacyTemplate = enableLegacyTemplate !== false;
this.preserveWhitespaces = preserveWhitespacesDefault(noUndefined(preserveWhitespaces));
}
}
export function preserveWhitespacesDefault(
preserveWhitespacesOption: boolean | null, defaultSetting = true): boolean {
return preserveWhitespacesOption === null ? defaultSetting : preserveWhitespacesOption;
}
@@ -9,7 +9,7 @@
import {ViewEncapsulation, ɵstringify as stringify} from '@angular/core';
import {CompileAnimationEntryMetadata, CompileDirectiveMetadata, CompileStylesheetMetadata, CompileTemplateMetadata, templateSourceUrl} from './compile_metadata';
import {CompilerConfig} from './config';
import {CompilerConfig, preserveWhitespacesDefault} from './config';
import {CompilerInjectable} from './injectable';
import * as html from './ml_parser/ast';
import {HtmlParser} from './ml_parser/html_parser';
@@ -31,6 +31,7 @@ export interface PrenormalizedTemplateMetadata {
interpolation: [string, string]|null;
encapsulation: ViewEncapsulation|null;
animations: CompileAnimationEntryMetadata[];
preserveWhitespaces: boolean|null;
}
@CompilerInjectable()
@@ -82,6 +83,13 @@ export class DirectiveNormalizer {
throw syntaxError(
`No template specified for component ${stringify(prenormData.componentType)}`);
}
if (isDefined(prenormData.preserveWhitespaces) &&
typeof prenormData.preserveWhitespaces !== 'boolean') {
throw syntaxError(
`The preserveWhitespaces option for component ${stringify(prenormData.componentType)} must be a boolean`);
}
return SyncAsync.then(
this.normalizeTemplateOnly(prenormData),
(result: CompileTemplateMetadata) => this.normalizeExternalStylesheets(result));
@@ -149,7 +157,9 @@ export class DirectiveNormalizer {
ngContentSelectors: visitor.ngContentSelectors,
animations: prenormData.animations,
interpolation: prenormData.interpolation, isInline,
externalStylesheets: []
externalStylesheets: [],
preserveWhitespaces: preserveWhitespacesDefault(
prenormData.preserveWhitespaces, this._config.preserveWhitespaces),
});
}
@@ -168,6 +178,7 @@ export class DirectiveNormalizer {
animations: templateMeta.animations,
interpolation: templateMeta.interpolation,
isInline: templateMeta.isInline,
preserveWhitespaces: templateMeta.preserveWhitespaces,
}));
}
@@ -152,7 +152,8 @@ export class DirectiveResolver {
styleUrls: directive.styleUrls,
encapsulation: directive.encapsulation,
animations: directive.animations,
interpolation: directive.interpolation
interpolation: directive.interpolation,
preserveWhitespaces: directive.preserveWhitespaces,
});
} else {
return new Directive({
@@ -262,6 +262,7 @@ export class JitCompiler implements Compiler {
const externalStylesheetsByModuleUrl = new Map<string, CompiledStylesheet>();
const outputContext = createOutputContext();
const componentStylesheet = this._styleCompiler.compileComponent(outputContext, compMeta);
const preserveWhitespaces = compMeta !.template !.preserveWhitespaces;
compMeta.template !.externalStylesheets.forEach((stylesheetMeta) => {
const compiledStylesheet =
this._styleCompiler.compileStyles(createOutputContext(), compMeta, stylesheetMeta);
@@ -274,7 +275,8 @@ export class JitCompiler implements Compiler {
pipe => this._metadataResolver.getPipeSummary(pipe.reference));
const {template: parsedTemplate, pipes: usedPipes} = this._templateParser.parse(
compMeta, compMeta.template !.template !, directives, pipes, template.ngModule.schemas,
templateSourceUrl(template.ngModule.type, template.compMeta, template.compMeta.template !));
templateSourceUrl(template.ngModule.type, template.compMeta, template.compMeta.template !),
preserveWhitespaces);
const compileResult = this._viewCompiler.compileComponent(
outputContext, compMeta, parsedTemplate, ir.variable(componentStylesheet.stylesVar),
usedPipes);
@@ -106,6 +106,7 @@ export class JitCompilerFactory implements CompilerFactory {
defaultEncapsulation: ViewEncapsulation.Emulated,
missingTranslation: MissingTranslationStrategy.Warning,
enableLegacyTemplate: true,
preserveWhitespaces: true,
};
this._defaultOptions = [compilerOptions, ...defaultOptions];
@@ -125,6 +126,7 @@ export class JitCompilerFactory implements CompilerFactory {
defaultEncapsulation: opts.defaultEncapsulation,
missingTranslation: opts.missingTranslation,
enableLegacyTemplate: opts.enableLegacyTemplate,
preserveWhitespaces: opts.preserveWhitespaces,
});
},
deps: []
@@ -152,6 +154,7 @@ function _mergeOptions(optionsArr: CompilerOptions[]): CompilerOptions {
providers: _mergeArrays(optionsArr.map(options => options.providers !)),
missingTranslation: _lastDefined(optionsArr.map(options => options.missingTranslation)),
enableLegacyTemplate: _lastDefined(optionsArr.map(options => options.enableLegacyTemplate)),
preserveWhitespaces: _lastDefined(optionsArr.map(options => options.preserveWhitespaces)),
};
}
@@ -219,7 +219,8 @@ export class CompileMetadataResolver {
styles: template.styles,
styleUrls: template.styleUrls,
animations: template.animations,
interpolation: template.interpolation
interpolation: template.interpolation,
preserveWhitespaces: template.preserveWhitespaces
});
if (isPromise(templateMeta) && isSync) {
this._reportError(componentStillLoadingError(directiveType), directiveType);
@@ -267,7 +268,8 @@ export class CompileMetadataResolver {
interpolation: noUndefined(dirMeta.interpolation),
isInline: !!dirMeta.template,
externalStylesheets: [],
ngContentSelectors: []
ngContentSelectors: [],
preserveWhitespaces: noUndefined(dirMeta.preserveWhitespaces),
});
}
@@ -0,0 +1,86 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as html from './ast';
import {ParseTreeResult} from './parser';
import {NGSP_UNICODE} from './tags';
export const PRESERVE_WS_ATTR_NAME = 'ngPreserveWhitespaces';
const SKIP_WS_TRIM_TAGS = new Set(['pre', 'template', 'textarea', 'script', 'style']);
function hasPreserveWhitespacesAttr(attrs: html.Attribute[]): boolean {
return attrs.some((attr: html.Attribute) => attr.name === PRESERVE_WS_ATTR_NAME);
}
/**
* Angular Dart introduced &ngsp; as a placeholder for non-removable space, see:
* https://github.com/dart-lang/angular/blob/0bb611387d29d65b5af7f9d2515ab571fd3fbee4/_tests/test/compiler/preserve_whitespace_test.dart#L25-L32
* In Angular Dart &ngsp; is converted to the 0xE500 PUA (Private Use Areas) unicode character
* and later on replaced by a space. We are re-implementing the same idea here.
*/
export function replaceNgsp(value: string): string {
// lexer is replacing the &ngsp; pseudo-entity with NGSP_UNICODE
return value.replace(new RegExp(NGSP_UNICODE, 'g'), ' ');
}
/**
* This visitor can walk HTML parse tree and remove / trim text nodes using the following rules:
* - consider spaces, tabs and new lines as whitespace characters;
* - drop text nodes consisting of whitespace characters only;
* - for all other text nodes replace consecutive whitespace characters with one space;
* - convert &ngsp; pseudo-entity to a single space;
*
* Removal and trimming of whitespaces have positive performance impact (less code to generate
* while compiling templates, faster view creation). At the same time it can be "destructive"
* in some cases (whitespaces can influence layout). Because of the potential of breaking layout
* this visitor is not activated by default in Angular 4 and people need to explicitly opt-in for
* whitespace removal. The default option for whitespace removal will be revisited post Angular 5
* and might be changed to "on" by default.
*/
class WhitespaceVisitor implements html.Visitor {
visitElement(element: html.Element, context: any): any {
if (SKIP_WS_TRIM_TAGS.has(element.name) || hasPreserveWhitespacesAttr(element.attrs)) {
// don't descent into elements where we need to preserve whitespaces
// but still visit all attributes to eliminate one used as a market to preserve WS
return new html.Element(
element.name, html.visitAll(this, element.attrs), element.children, element.sourceSpan,
element.startSourceSpan, element.endSourceSpan);
}
return new html.Element(
element.name, element.attrs, html.visitAll(this, element.children), element.sourceSpan,
element.startSourceSpan, element.endSourceSpan);
}
visitAttribute(attribute: html.Attribute, context: any): any {
return attribute.name !== PRESERVE_WS_ATTR_NAME ? attribute : null;
}
visitText(text: html.Text, context: any): any {
const isBlank = text.value.trim().length === 0;
if (!isBlank) {
return new html.Text(replaceNgsp(text.value).replace(/\s\s+/g, ' '), text.sourceSpan);
}
return null;
}
visitComment(comment: html.Comment, context: any): any { return comment; }
visitExpansion(expansion: html.Expansion, context: any): any { return expansion; }
visitExpansionCase(expansionCase: html.ExpansionCase, context: any): any { return expansionCase; }
}
export function removeWhitespaces(htmlAstWithErrors: ParseTreeResult): ParseTreeResult {
return new ParseTreeResult(
html.visitAll(new WhitespaceVisitor(), htmlAstWithErrors.rootNodes),
htmlAstWithErrors.errors);
}
@@ -71,6 +71,7 @@ export function mergeNsAndName(prefix: string, localName: string): string {
// This list is not exhaustive to keep the compiler footprint low.
// The `&#123;` / `&#x1ab;` syntax should be used when the named character reference does not
// exist.
export const NAMED_ENTITIES: {[k: string]: string} = {
'Aacute': '\u00C1',
'aacute': '\u00E1',
@@ -325,3 +326,9 @@ export const NAMED_ENTITIES: {[k: string]: string} = {
'zwj': '\u200D',
'zwnj': '\u200C',
};
// The &ngsp; pseudo-entity is denoting a space. see:
// https://github.com/dart-lang/angular/blob/0bb611387d29d65b5af7f9d2515ab571fd3fbee4/_tests/test/compiler/preserve_whitespace_test.dart
export const NGSP_UNICODE = '\uE500';
NAMED_ENTITIES['ngsp'] = NGSP_UNICODE;
Oops, something went wrong.

0 comments on commit b8b551c

Please sign in to comment.