diff --git a/packages/compiler-cli/src/ngtsc/transform/src/transform.ts b/packages/compiler-cli/src/ngtsc/transform/src/transform.ts index 99933680d0..af1e79be95 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/transform.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/transform.ts @@ -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: diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index e5fac6559c..6e6848a80d 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -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', () => { diff --git a/packages/compiler/src/constant_pool.ts b/packages/compiler/src/constant_pool.ts index b083a2c1ed..ae9f7c3009 100644 --- a/packages/compiler/src/constant_pool.ts +++ b/packages/compiler/src/constant_pool.ts @@ -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; -} \ No newline at end of file +}