refactor(compiler): wrap large strings in function (#38253)

Large strings constants are now wrapped in a function which is called whenever used. This works around a unique
limitation of Closure, where it will **always** inline string literals at **every** usage, regardless of how large the
string literal is or how many times it is used.The workaround is to use a function rather than a string literal.
Closure has differently inlining semantics for functions, where it will check the length of the function and the number
of times it is used before choosing to inline it. By using a function, `ngtsc` makes Closure more conservative about
inlining large strings, and avoids blowing up the bundle size.This optimization is only used if the constant is a large
string. A wrapping function is not included for other use cases, since it would just increase the bundle size and add
unnecessary runtime performance overhead.

PR Close #38253
This commit is contained in:
Doug Parker 2020-07-27 13:24:55 -07:00 committed by Alex Rickabaugh
parent 103a95c182
commit 887c350f9d
3 changed files with 100 additions and 8 deletions

View File

@ -249,7 +249,7 @@ function transformIvySourceFile(
importRewriter: ImportRewriter, file: ts.SourceFile, isCore: boolean,
isClosureCompilerEnabled: boolean,
defaultImportRecorder: DefaultImportRecorder): ts.SourceFile {
const constantPool = new ConstantPool();
const constantPool = new ConstantPool(isClosureCompilerEnabled);
const importManager = new ImportManager(importRewriter);
// The transformation process consists of 2 steps:

View File

@ -6685,6 +6685,66 @@ export const Foo = Foo__PRE_R3__;
// remains a string in the `styles` array.
'"img[_ngcontent-%COMP%] { background: url(/b/some-very-very-long-path.png); }"]');
});
it('large strings are wrapped in a function for Closure', () => {
env.tsconfig({
annotateForClosureCompiler: true,
});
env.write('test.ts', `
import {Component} from '@angular/core';
@Component({
selector: 'comp-a',
template: 'Comp A',
styles: [
'div { background: url(/a.png); }',
'div { background: url(/some-very-very-long-path.png); }',
]
})
export class CompA {}
@Component({
selector: 'comp-b',
template: 'Comp B',
styles: [
'div { background: url(/b.png); }',
'div { background: url(/some-very-very-long-path.png); }',
]
})
export class CompB {}
`);
env.driveMain();
const jsContents = env.getContents('test.js');
// Verify that long strings are extracted to a separate var. This should be wrapped in a
// function to trick Closure not to inline the contents for very large strings.
// See: https://github.com/angular/angular/pull/38253.
expect(jsContents)
.toContain(
'_c0 = function () {' +
' return "div[_ngcontent-%COMP%] {' +
' background: url(/some-very-very-long-path.png);' +
' }";' +
' };');
expect(jsContents)
.toContain(
'styles: [' +
// Check styles for component A.
'"div[_ngcontent-%COMP%] { background: url(/a.png); }", ' +
// Large string should be called from function definition.
'_c0()]');
expect(jsContents)
.toContain(
'styles: [' +
// Check styles for component B.
'"div[_ngcontent-%COMP%] { background: url(/b.png); }", ' +
// Large string should be called from function definition.
'_c0()]');
});
});
describe('non-exported classes', () => {

View File

@ -102,8 +102,10 @@ export class ConstantPool {
private nextNameIndex = 0;
constructor(private readonly isClosureCompilerEnabled: boolean = false) {}
getConstLiteral(literal: o.Expression, forceShared?: boolean): o.Expression {
if ((literal instanceof o.LiteralExpr && !isLongStringExpr(literal)) ||
if ((literal instanceof o.LiteralExpr && !isLongStringLiteral(literal)) ||
literal instanceof FixupExpression) {
// Do no put simple literals into the constant pool or try to produce a constant for a
// reference to a constant.
@ -121,9 +123,39 @@ export class ConstantPool {
if ((!newValue && !fixup.shared) || (newValue && forceShared)) {
// Replace the expression with a variable
const name = this.freshName();
this.statements.push(
o.variable(name).set(literal).toDeclStmt(o.INFERRED_TYPE, [o.StmtModifier.Final]));
fixup.fixup(o.variable(name));
let definition: o.WriteVarExpr;
let usage: o.Expression;
if (this.isClosureCompilerEnabled && isLongStringLiteral(literal)) {
// For string literals, Closure will **always** inline the string at
// **all** usages, duplicating it each time. For large strings, this
// unnecessarily bloats bundle size. To work around this restriction, we
// wrap the string in a function, and call that function for each usage.
// This tricks Closure into using inline logic for functions instead of
// string literals. Function calls are only inlined if the body is small
// enough to be worth it. By doing this, very large strings will be
// shared across multiple usages, rather than duplicating the string at
// each usage site.
//
// const myStr = function() { return "very very very long string"; };
// const usage1 = myStr();
// const usage2 = myStr();
definition = o.variable(name).set(new o.FunctionExpr(
[], // Params.
[
// Statements.
new o.ReturnStatement(literal),
],
));
usage = o.variable(name).callFn([]);
} else {
// Just declare and use the variable directly, without a function call
// indirection. This saves a few bytes and avoids an unncessary call.
definition = o.variable(name).set(literal);
usage = o.variable(name);
}
this.statements.push(definition.toDeclStmt(o.INFERRED_TYPE, [o.StmtModifier.Final]));
fixup.fixup(usage);
}
return fixup;
@ -314,7 +346,7 @@ function isVariable(e: o.Expression): e is o.ReadVarExpr {
return e instanceof o.ReadVarExpr;
}
function isLongStringExpr(expr: o.LiteralExpr): boolean {
return typeof expr.value === 'string' &&
function isLongStringLiteral(expr: o.Expression): boolean {
return expr instanceof o.LiteralExpr && typeof expr.value === 'string' &&
expr.value.length >= POOL_INCLUSION_LENGTH_THRESHOLD_FOR_STRINGS;
}
}