Building an Angular App for Production

82bafb0432ce4ccc9dcc26f94d5fe5bc?s=47 Minko Gechev
November 11, 2016

Building an Angular App for Production

In the world of the single-page applications we usually have to transfer huge amount of resources over the wire which dramatically impacts the initial load time. On the other hand, high-performance applications always bring better user engagement and experience. We often implicitly judge the quality of given app by it’s initial load time and responsiveness.

In this talk we’re going explain essential practices that can help us reduce the initial load time of our Angular applications. We will go through bundling, dead-code elimination, tree-shaking and complete our journey with Ahead-of-Time compilation. We're going to explore "How we can make Angular small again"!

82bafb0432ce4ccc9dcc26f94d5fe5bc?s=128

Minko Gechev

November 11, 2016
Tweet

Transcript

  1. Building an Angular App for Production Minko Gechev github.com/mgechev twitter.com/mgechev

    blog.mgechev.com
  2. None
  3. None
  4. Hold on for the second edition!

  5. None
  6. Inspirational thought about programming Agenda • Bundling • Minification •

    Tree-shaking • AoT compilation • Compression
  7. Building “Hello world” in Angular 2

  8. <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title></title> </head> <body>

    <my-app></my-app> <script src="/node_modules/zone.js/dist/zone.js"></script> <script src="/node_modules/reflect-metadata/Reflect.js"></script> <script src="dist/bundle.js"></script> </body> </html> index.html
  9. <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title></title> </head> <body>

    <my-app></my-app> <script src="/node_modules/zone.js/dist/zone.js"></script> <script src="/node_modules/reflect-metadata/Reflect.js"></script> <script src="dist/bundle.js"></script> </body> </html> index.html
  10. app.component.ts import { Component } from '@angular/core'; @Component({ selector: 'my-app',

    template: 'Hello world!' }) export class AppComponent {}
  11. import { NgModule } from '@angular/core'; import { BrowserModule }

    from '@angular/platform-browser'; import { AppComponent } from './app.component'; @NgModule({ imports: [BrowserModule], bootstrap: [AppComponent], declarations: [AppComponent], }) export class AppModule {} app.module.ts
  12. main.ts import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule

    } from './app.module'; platformBrowserDynamic().bootstrapModule(AppModule);
  13. Bundling

  14. “…standard practice aiming to reduce the number of requests that

    the browser needs to perform in order to deliver the application requested by the user”
  15. None
  16. None
  17. Less requests so our application gets faster

  18. $ browserify -s main main.js > bundle.js

  19. …one big blocking request

  20. …one big blocking request

  21. …okay but that’s a “Hello world” app

  22. “Hello world” app in Angular 2

  23. “Hello world” app in Angular 2

  24. Building “Hello world” in Angular 2 with minimal size

  25. Building “Hello world” in Angular 2 with minimal size

  26. Minification & Dead Code Elimination

  27. Minification & dead code elimination • Mangling • Rename variable

    names • Rename property names • others… • Compression • Dead code elimination • Compress property access • Compress expressions • Pure functions • others…
  28. Dead code elimination

  29. dummy.js function answer() { const expr = true; if (expr)

    { return 42; } return 1.618; } console.log(answer());
  30. function answer() { const expr = true; if (true) {

    return 42; } return 1.618; } console.log(answer()); dummy.js
  31. function answer() { const expr = true; if (true) {

    return 42; } return 1.618; } console.log(answer()); dummy.js
  32. console.log(42); dummy.js

  33. …usually we can get rid of small portions of unused

    code
  34. $ uglifyjs bundle.js --output bundle.min.js $ ls bundle.min.js 524K Oct

    15 18:41 bundle.min.js
  35. $ uglifyjs bundle.js --output bundle.min.js $ ls bundle.min.js 524K Oct

    15 18:41 bundle.min.js
  36. $ uglifyjs bundle.js --output bundle.min.js $ ls bundle.min.js 524K Oct

    15 18:41 bundle.min.js
  37. None
  38. 3x smaller but it’s still huge…

  39. Bundling CommonJS Modules

  40. index.js lib.js module.exports.foo = () => 'foo'; module.exports.bar = ()

    => 'bar'; const foo = require('./lib').foo; console.log(foo());
  41. index.js module.exports.foo = () => 'foo'; module.exports.bar = () =>

    'bar'; lib.js const foo = require('./lib').foo; console.log(foo());
  42. index.js const foo = require('./lib').foo; console.log(foo()); lib.js module.exports.foo = ()

    => 'foo'; module.exports.bar = () => 'bar';
  43. bundle.js module.exports.foo = () => 'foo'; module.exports.bar = () =>

    'bar'; const foo = require('./lib').foo; console.log(foo());
  44. Why is that?

  45. index.js const readFile = require('fs').readFileSync; const funcName = readFile('name.txt') .toString().trim();

    const fn = require('./lib')[funcName]; console.log(fn());
  46. const readFile = require('fs').readFileSync; const funcName = readFile('name.txt') .toString().trim(); const

    fn = require('./lib')[funcName]; console.log(fn()); index.js
  47. const readFile = require('fs').readFileSync; const funcName = readFile('name.txt') .toString().trim(); const

    fn = require('./lib')[funcName]; console.log(fn()); index.js
  48. CommonJS modules are hard for static code analysis because they

    are dynamic
  49. ES2015 modules are static so they can be statically analyzed

    easier
  50. lib.js index.js export foo = () => 'foo'; export bar

    = () => 'bar'; import { foo } from './lib'; console.log(foo());
  51. export foo = () => 'foo'; export bar = ()

    => 'bar'; import { foo } from './lib'; console.log(foo()); index.js lib.js
  52. export foo = () => 'foo'; export bar = ()

    => 'bar'; import { foo } from './lib'; console.log(foo()); index.js lib.js
  53. export foo = () => 'foo'; export bar = ()

    => 'bar'; import { foo } from `./${name}`; console.log(foo()); index.js lib.js
  54. export foo = () => 'foo'; export bar = ()

    => 'bar'; import { foo } from `./${name}`; console.log(foo()); index.js lib.js
  55. bundle.js let foo = () => 'foo'; console.log(foo());

  56. ES2015 modules • Exports are always static • Identifiers •

    Default exports • Imports are always static • Strings as paths • Importing specific symbols referenced by their identifiers • Wildcard (the entire module) • Importing the default export
  57. Tree-shaking

  58. …dropping unused exports

  59. Tooling • Rollup.js • Supports ES2015 modules • Has plugin

    for commonjs • Webpack 2 • Performs tree-shaking over ES2015 • Google Closure Compiler • Limited ES2015 modules support
  60. Lets roll it up!

  61. $ rollup -c config.js -o bundle.es2015.js $ tsc --target es5

    --allowJs bundle.es2015.js $ uglifyjs bundle.js --output bundle.min.js $ ls bundle.min.js 502K Oct 15 18:41 bundle.min.js
  62. $ rollup -c config.js -o bundle.es2015.js $ tsc --target es5

    --allowJs bundle.es2015.js $ uglifyjs bundle.js --output bundle.min.js $ ls bundle.min.js 502K Oct 15 18:41 bundle.min.js
  63. $ rollup -c config.js -o bundle.es2015.js $ tsc --target es5

    --allowJs bundle.es2015.js $ uglifyjs bundle.js --output bundle.min.js $ ls bundle.min.js 502K Oct 15 18:41 bundle.min.js
  64. $ rollup -c config.js -o bundle.es2015.js $ tsc --target es5

    --allowJs bundle.es2015.js $ uglifyjs bundle.js --output bundle.min.js $ ls bundle.min.js 502K Oct 15 18:41 bundle.min.js
  65. $ rollup -c config.js -o bundle.es2015.js $ tsc --target es5

    --allowJs bundle.es2015.js $ uglifyjs bundle.js --output bundle.min.js $ ls bundle.min.js 502K Oct 15 18:41 bundle.min.js
  66. None
  67. tree-shaking templates

  68. <div class="hero"></div> <div *ngIf="showLogin" class="login"> Existing users: <a href="#" [routerLink]="['login']">

    Login here </a> </div> How many directives do we have here?
  69. <div class="hero"></div> <div *ngIf="showLogin" class="login"> Existing users: <a href="#" [routerLink]="['login']">

    Login here </a> </div> How many directives do we have here?
  70. @Component({ selector: 'div.hero', template: '<h1>{{ hero.name }}' }) class SuperheroComponent

    { @Input() hero: Superhero; } superhero.component.ts
  71. <div class="hero"></div> <div *ngIf="showLogin" class="login"> Existing users: <a href="#" [routerLink]="['login']">

    Login here </a> </div> How many directives do we have here?
  72. At Runtime we know which directives are used in our

    templates
  73. app.component.ts import { Component } from '@angular/core'; @Component({ selector: 'my-app',

    template: 'Hello world!' }) export class AppComponent {}
  74. NgIf, NgFor and other useless directives are still in our

    bundle
  75. We cannot truly tree-shake an Angular Application?

  76. …of course we can…

  77. Introducing ngc

  78. <div class="hero"></div> <div *ngIf="showLogin" class="login"> Existing users: <a href="#" [routerLink]="['login']">

    Login here </a> </div>
  79. import { NgIf, AppElement, TemplateRef_ } from "…"; ... this._anchor_11

    = r.createTemplateAnchor(this._el_5,(null as any)); this._appEl_11 = AppElement(11,5,this,this._anchor_11); this._TemplateRef_11_5 = TemplateRef_(this._appEl_11, HeaderComponent1); this._NgIf_11_6 = NgIf(this._appEl_11.vcRef, this._TemplateRef_11_5); this._text_12 = r.createText(this._el_5,'\n ',(null as any)); ... <div class="hero"></div> <div *ngIf="showLogin" class="login"> Existing users: <a href="#" [routerLink]="['login']"> Login here </a> </div>
  80. import { NgIf, AppElement, TemplateRef_ } from "…"; ... this._anchor_11

    = r.createTemplateAnchor(this._el_5,(null as any)); this._appEl_11 = AppElement(11,5,this,this._anchor_11); this._TemplateRef_11_5 = TemplateRef_(this._appEl_11, HeaderComponent1); this._NgIf_11_6 = NgIf(this._appEl_11.vcRef, this._TemplateRef_11_5); this._text_12 = r.createText(this._el_5,'\n ',(null as any)); ... <div class="hero"></div> <div *ngIf="showLogin" class="login"> Existing users: <a href="#" [routerLink]="['login']"> Login here </a> </div>
  81. Angular’s Ahead-of-Time (AoT) compilation

  82. AoT compilation Brings • Easy drop of unused components in

    bundle • Code which is easy for dead-code elimination • Fast initial rendering • Components are distributed pre-compiled • Allows us to drop compiler from bundler • We compile ahead of time so we don’t need it runtime
  83. $ ./node_modules/.bin/ngc -p tsconfig.json $ tsc -p tsconfig.json $ rollup

    -c config.js -o bundle.es2015.js $ tsc --target es5 --allowJs bundle.es2015.js $ uglifyjs bundle.js --output bundle.min.js $ ls bundle.min.js 210K Oct 15 18:41 bundle.min.js
  84. $ ./node_modules/.bin/ngc -p tsconfig.json $ tsc -p tsconfig.json $ rollup

    -c config.js -o bundle.es2015.js $ tsc --target es5 --allowJs bundle.es2015.js $ uglifyjs bundle.js --output bundle.min.js $ ls bundle.min.js 210K Oct 15 18:41 bundle.min.js
  85. $ ./node_modules/.bin/ngc -p tsconfig.json $ tsc -p tsconfig.json $ rollup

    -c config.js -o bundle.es2015.js $ tsc --target es5 --allowJs bundle.es2015.js $ uglifyjs bundle.js --output bundle.min.js $ ls bundle.min.js 210K Oct 15 18:41 bundle.min.js
  86. $ ./node_modules/.bin/ngc -p tsconfig.json $ tsc -p tsconfig.json $ rollup

    -c config.js -o bundle.es2015.js $ tsc --target es5 --allowJs bundle.es2015.js $ uglifyjs bundle.js --output bundle.min.js $ ls bundle.min.js 210K Oct 15 18:41 bundle.min.js
  87. None
  88. Disclaimer ngc will not always decrease your bundle size. For

    large to medium applications the produced from the compilation JavaScript increases the app size.
  89. Compression

  90. Client Server

  91. Client Server GET /app.js Accept-Encoding: deflate, br

  92. Client Server GET /app.js Accept-Encoding: deflate, br HTTP/1.1 200 OK

    Content-Encoding: br …
  93. Client Server GET /app.js Accept-Encoding: deflate, br HTTP/1.1 200 OK

    Content-Encoding: br … bro --decompress app.js
  94. Popular algorithms • Deflate • Widely supported algorithm • Gzip

    + checksum in header/footer • Brotli • Similar runtime performance compared to deflate • Provides more dense compression compared to deflate • Not widely supported
  95. $ bro --force --input b.js --output b.brotli $ ls b.brotli

    39K Oct 15 18:38 b.brotli
  96. $ bro --force --input b.js --output b.brotli $ ls b.brotli

    39K Oct 15 18:38 b.brotli
  97. $ bro --force --input b.js --output b.brotli $ ls b.brotli

    39K Oct 15 18:38 b.brotli
  98. None
  99. 36x smaller compared to the initial bundle!

  100. lets try one more thing

  101. Google Closure Compiler

  102. /** * @param {string} name * @return {Superhero} */ function

    heroFactory(name) { return new Superhero(name, 3); } hero.ts
  103. $ tsickle function heroFactory(name: string) { return new Superhero(name, 3);

    } /** * @param {string} name * @return {Superhero} */ function heroFactory(name) { return new Superhero(name, 3); }
  104. $ tsickle function heroFactory(name: string) { return new Superhero(name, 3);

    } /** * @param {string} name * @return {Superhero} */ function heroFactory(name) { return new Superhero(name, 3); }
  105. $ tsickle function heroFactory(name: string) { return new Superhero(name, 3);

    } /** * @param {string} name * @return {Superhero} */ function heroFactory(name) { return new Superhero(name, 3); }
  106. tsickle transpiles TypeScript to Closure compatible annotations

  107. $ ls bundle.brotli 39K Oct 15 18:38 b.brotli

  108. $ ls bundle.brotli 32K Oct 15 18:38 bundle.brotli

  109. None
  110. Tools we used • tsc • Browserify • UglifyJS •

    Rollup.js • ngc • Google Closure Compiler • Brotli
  111. None
  112. None
  113. Here Here Here

  114. None
  115. None
  116. None
  117. None
  118. None
  119. None
  120. …all this in angular-cli # build an app with env

    config ng build --prod --env=prod …in angular-seed # build with AoT npm run build.prod.exp
  121. Resources • mgv.io/ng-perf-checklist • mgv.io/build-ng-prod • mgv.io/mb-toolkit • mgv.io/n2cli •

    mgv.io/ng-seed
  122. mgv.io/ng4prod-feedback

  123. Thank you! github.com/mgechev twitter.com/mgechev blog.mgechev.com