Angular Performance Checklist

Angular Performance Checklist

High performance applications always bring better user engagement and experience. We often implicitly judge the quality of given application by it’s initial load time and responsiveness. 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 top of that, performing change detection over the entire component tree, corresponding to a complex UI, often causes frame drops because of heavy computations happening in the main thread. In the first part of this talk we’re going explain essential practices that can help us reduce the initial load time of our Angular applications.

82bafb0432ce4ccc9dcc26f94d5fe5bc?s=128

Minko Gechev

October 21, 2016
Tweet

Transcript

  1. Angular Performance Checklist Minko Gechev github.com/mgechev twitter.com/mgechev blog.mgechev.com

  2. github.com/mgechev twitter.com/mgechev blog.mgechev.com About deprecated

  3. None
  4. Hold on for the second edition!

  5. Inspirational thought about programming https://flic.kr/p/bmgpJT Agenda Network performance Runtime performance

  6. Inspirational thought about programming https://flic.kr/p/bmgpJT Agenda Network performance Runtime performance

  7. Network Performance

  8. • Service Workers • Bundling • Minification & Dead code

    elimination • Tree-shaking • AoT compilation • Compression Network Performance
  9. None
  10. Build a “Hello world” Angular 2 Application with minimal size

  11. Build a “Hello world” Angular 2 Application with minimal size

  12. None
  13. “Hello world” in Angular 2

  14. <!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
  15. <!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
  16. app.component.ts import { Component } from '@angular/core'; @Component({ selector: 'my-app',

    template: 'Hello world!' }) export class AppComponent {}
  17. 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
  18. main.ts import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule

    } from './app.module'; platformBrowserDynamic().bootstrapModule(AppModule);
  19. None
  20. Service Workers

  21. - Jake Archibald “ServiceWorker is a background worker, it gives

    us a JavaScript context to add features such as push messaging, background sync, geofencing and network control.”
  22. - Jake Archibald “ServiceWorker is a background worker, it gives

    us a JavaScript context to add features such as push messaging, background sync, geofencing and network control.”
  23. Service Worker Network

  24. Service Worker Network GET /i.gif

  25. Service Worker Network GET /i.gif

  26. Service Worker Network

  27. Service Worker Network

  28. We can think of service workers as proxy in the

    browser
  29. index.html <!DOCTYPE html> <html lang="en"> ... <body> ... <script> navigator.serviceWorker

    .register('cache-network.sw.js'); </script> </body> </html>
  30. cache-network.sw.js importScripts('./system-config.js'); self.addEventListener('install', event => { event.waitUntil( caches.open('mysite-v3').then(cache => cache.addAll([

    'zone.js' // etc ]))); }); self.addEventListener(‘fetch', event => { event.respondWith(caches .match(event.request).then(response => response || fetch(event.request))); });
  31. cache-network.sw.js importScripts('./system-config.js'); self.addEventListener('install', event => { event.waitUntil( caches.open('mysite-v3').then(cache => cache.addAll([

    'zone.js' // etc ]))); }); self.addEventListener('fetch', event => { event.respondWith(caches .match(event.request).then(response => response || fetch(event.request))); });
  32. cache-network.sw.js importScripts('./system-config.js'); self.addEventListener('install', event => { event.waitUntil( caches.open('mysite-v3').then(cache => cache.addAll([

    'zone.js' // etc ]))); }); self.addEventListener('fetch', event => { event.respondWith(caches .match(event.request).then(response => response || fetch(event.request))); });
  33. cache-network.sw.js importScripts('./system-config.js'); self.addEventListener('install', event => { event.waitUntil( caches.open('mysite-v3').then(cache => cache.addAll([

    'zone.js' // etc ]))); }); self.addEventListener('fetch', event => { event.respondWith(caches .match(event.request).then(response => response || fetch(event.request))); });
  34. Angular Mobile Toolkit makes it easier

  35. index.html <!DOCTYPE html> <html lang="en"> ... <body> <script> navigator.serviceWorker .register('/worker-basic.js');

    </script> </body> </html>
  36. ngsw-manifest.json { "static": { "urls": { "zone.js": "00ea1da4192a2030f9ae023de3b3143ed647bbab", "Reflect.js": "c48ea7a8ab46d67174f708d2464a46aa523d218d",

    "bundle.js": "25afd671e9ad333887e2ef79b34297125a6a0b6a" }, "_generatedFromWebpack": true } }
  37. You can generate the manifest with webpack!

  38. Bundling

  39. “…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”
  40. None
  41. None
  42. Less requests, so latency has smaller impact

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

  44. …one big blocking request

  45. …one big blocking request

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

  47. “Hello world” app in Angular 2

  48. “Hello world” app in Angular 2

  49. Minification & Dead Code Elimination

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

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

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

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

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

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

  56. Usually we can get rid of small portions of unused

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

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

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

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

  62. Bundling CommonJS Modules

  63. module.exports.pow = (num, pow) => Math.pow(num, pow); module.exports.log2 = num

    => Math.log2(num); index.js math.js const pow = require('./math').pow; console.log(pow(2, 3));
  64. module.exports.pow = (num, pow) => Math.pow(num, pow); module.exports.log2 = num

    => Math.log2(num); index.js math.js const pow = require('./math').pow; console.log(pow(2, 3));
  65. module.exports.pow = (num, pow) => Math.pow(num, pow); module.exports.log2 = num

    => Math.log2(num); index.js math.js const pow = require('./math').pow; console.log(pow(2, 3));
  66. bundle.js module.exports.pow = (num, pow) => Math.pow(num, pow); module.exports.log2 =

    num => Math.log2(num); const pow = require('./math').pow; console.log(pow(2, 3));
  67. Why is that?

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

    const fn = require('./math')[funcName]; console.log(fn(2, 42));
  69. bundle.js const readFile = require('fs').readFileSync; const funcName = readFile('name.txt') .toString().trim();

    const fn = require('./math')[funcName]; console.log(fn(2, 42));
  70. bundle.js const readFile = require('fs').readFileSync; const funcName = readFile('name.txt') .toString().trim();

    const fn = require('./math')[funcName]; console.log(fn(2, 42));
  71. CommonJS modules are Hard for Static Code Analysis because they

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

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

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

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

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

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

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

  79. 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
  80. Tree-shaking

  81. …dropping unused exports

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

    for commonjs • Webpack 2 • Performs tree-shaking over ES2015 • Google Closure Compiler • Limited ES2015 modules support • Can use TypeScript type annotations with tsickle
  83. Lets roll it up!

  84. $ 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
  85. $ 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
  86. $ 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
  87. $ 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
  88. $ 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
  89. None
  90. tree-shaking templates

  91. <div class="bp-logo"></div> <div *ngIf="showLogin" class="login"> Existing users: <a href="#" [routerLink]="['login']">

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

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

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

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

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

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

    bundle
  98. We Cannot Truly Tree-Shake an Angular Application?

  99. …of course we can…

  100. Introducing ngc

  101. <div class="bp-logo"></div> <div *ngIf="showLogin" class="login"> Existing users: <a href="#" [routerLink]="['login']">

    Login here </a> </div>
  102. 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="bp-logo"></div> <div *ngIf="showLogin" class="login"> Existing users: <a href="#" [routerLink]="['login']"> Login here </a> </div>
  103. 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="bp-logo"></div> <div *ngIf="showLogin" class="login"> Existing users: <a href="#" [routerLink]="['login']"> Login here </a> </div>
  104. Angular’s Ahead-of-Time (AoT) compilation

  105. AoT 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
  106. $ ./node_modules/.bin/ngc -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
  107. $ ./node_modules/.bin/ngc -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
  108. $ ./node_modules/.bin/ngc -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
  109. $ ./node_modules/.bin/ngc -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
  110. None
  111. Disclaimer ngc will not always decrease your bundle size. For

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

  113. Client Server

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

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

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

    Content-Encoding: br … bro --decompress app.js
  117. 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
  118. $ bro --force --input b.js --output b.brotli $ ls b.brotli

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

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

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

  123. None
  124. …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
  125. Resources: • mgv.io/ng-perf-checklist • mgv.io/build-ng-prod • mgv.io/mb-toolkit • github.com/angular/angular-cli •

    mgv.io/ng-seed
  126. mgv.io/perf-checklist-feedback

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