Faster Angular Applications

Faster Angular Applications

One thing is sure - performance matters! Blocking the main thread of an application, causing frame drops, is one of the most efficient ways to get rid of a significant portion of our users.

Fortunately, the day could be saved, thanks to functional programming. Coming with, at first, abstract ideas, functional programming brings the concepts of immutability and purity, which combined can dramatically improve the rendering performance of our application.

In this talk, we'll apply concepts from functional programming in the face of pure components, pure pipes, and persistent data structures. We'll demonstrate in details how we can use them to build high-performant Angular applications.

82bafb0432ce4ccc9dcc26f94d5fe5bc?s=128

Minko Gechev

June 22, 2018
Tweet

Transcript

  1. twitter.com/mgechev github.com/mgechev blog.mgechev.com Faster Applications Minko Gechev

  2. twitter.com/mgechev

  3. twitter.com/mgechev twitter.com/mgechev github.com/mgechev

  4. twitter.com/mgechev

  5. twitter.com/mgechev JavaScript is powerful

  6. twitter.com/mgechev Complex business applications

  7. twitter.com/mgechev Everything comes with a price tag

  8. twitter.com/mgechev

  9. twitter.com/mgechev

  10. twitter.com/mgechev

  11. twitter.com/mgechev

  12. twitter.com/mgechev

  13. None
  14. None
  15. twitter.com/mgechev

  16. twitter.com/mgechev • Build optimizer • Tree-shakable providers • Compression •

    Minification Angular (CLI) does much more!
  17. twitter.com/mgechev Why should we even care?

  18. twitter.com/mgechev

  19. twitter.com/mgechev

  20. twitter.com/mgechev https://medium.com/dev-channel/the-cost-of-javascript-84009f51e99e

  21. twitter.com/mgechev Application specific optimizations

  22. twitter.com/mgechev lazy-loading

  23. twitter.com/mgechev Lazy-loading strategies Route-Based Component-Based

  24. twitter.com/mgechev Lazy-loading strategies Route-Based Component-Based

  25. export const appRoutes: Routes = [{ path: '', pathMatch: 'full',

    redirectTo: 'intro' }, { loadChildren: './intro/intro.module#IntroModule', path: 'intro' }, { loadChildren: './main/main.module#MainModule', path: 'main' }];
  26. twitter.com/mgechev Lazy-load all the things!

  27. twitter.com/mgechev • Bad UX for subsequent page views • Structural

    changes • Behavioral changes • Nested routes • Cross-bundle dependencies • Webpack’s extra bundle runtime Code-split gotchas
  28. twitter.com/mgechev Lazy-load all the things!??

  29. twitter.com/mgechev • Carefully pick the split points • Use prefetching

    Improve lazy-loading UX
  30. twitter.com/mgechev How do we pick the split points?

  31. twitter.com/mgechev often subjectively

  32. twitter.com/mgechev Data-driven bundling - Users behavior - Bundle sizes

  33. twitter.com/mgechev Data-driven bundling - Users behavior - Bundle sizes

  34. twitter.com/mgechev Step 1: Open https://example.com/ Step 2: Determine JavaScript which

    is likely to be required Step 3: Download the chunks Step 4: Store chunks in browser cache Pre-fetching
  35. twitter.com/mgechev Prefetching Strategies

  36. @NgModule({ bootstrap: [MailAppCmp], imports: [ RouterModule.forRoot(ROUTES, { preloadingStrategy: PreloadAllModules })

    ] }) class MailModule {}
  37. @NgModule({ bootstrap: [MailAppCmp], imports: [ RouterModule.forRoot(ROUTES, { preloadingStrategy: PreloadAllModules })

    ] }) class MailModule {}
  38. twitter.com/mgechev Draining user’s network connection

  39. export const appRoutes: Routes = [{ path: '', pathMatch: 'full',

    redirectTo: 'intro' }, { loadChildren: './intro/intro.module#IntroModule', path: 'intro', data: { preload: true } }, { loadChildren: './main/main.module#MainModule', path: 'main' }];
  40. export const appRoutes: Routes = [{ path: '', pathMatch: 'full',

    redirectTo: 'intro' }, { loadChildren: './intro/intro.module#IntroModule', path: 'intro', data: { preload: true } }, { loadChildren: './main/main.module#MainModule', path: 'main' }];
  41. export class PreloadSelctedModulesList implements PreloadingStrategy { preload(route: Route, load: Function):

    Observable<any> { return route.data.preload ? load() : of(null); } }
  42. twitter.com/mgechev How do we pick the priorities of the bundles

    to pre-fetch?
  43. twitter.com/mgechev

  44. twitter.com/mgechev What if the application changes?

  45. twitter.com/mgechev Update the app Collect data Update prefetch logic

  46. twitter.com/mgechev Update the app Collect data Update prefetch logic manual

    work
  47. twitter.com/mgechev Nested route resolution

  48. twitter.com/mgechev /a /a/a /a/b /b /b/a / twitter.com/mgechev

  49. twitter.com/mgechev /a /a/a /a/b /b /b/a / twitter.com/mgechev

  50. twitter.com/mgechev /a /a/a /a/b /b /b/a / twitter.com/mgechev

  51. twitter.com/mgechev /a /a/a /a/b /b /b/a / twitter.com/mgechev

  52. twitter.com/mgechev /a /a/a /a/b /b /b/a / twitter.com/mgechev

  53. twitter.com/mgechev Cluster bundles associated with nested routes, when it makes

    sense
  54. twitter.com/mgechev Cluster bundles associated with nested routes, when it makes

    sense
  55. twitter.com/mgechev Cluster bundles associated with nested routes, when it makes

    sense when the bundles are often requested together
  56. twitter.com/mgechev Cluster bundles associated with nested routes, when it makes

    sense when the bundles are often requested together when bundles are not too big
  57. twitter.com/mgechev • Lazy-load! • Lazy-load based on data • Overfetching

    matters • Use data-driven prefetching of assets • Beware of cascading route resolution • Use link[rel=“preload|prefetch”] Tips on lazy-loading
  58. twitter.com/mgechev Tooling

  59. twitter.com/mgechev • Manage the bundle sizes • Prefetch only the

    bundles likely to be used • Cluster small bundles in nested routes The tooling should
  60. twitter.com/mgechev Prefetch tooling for webpack webpack 4 preload-webpack-plugin import( !/*

    webpackPrefetch: true !*/ '!!...'); plugins: [ new HtmlWebpackPlugin(), new PreloadWebpackPlugin() ]
  61. twitter.com/mgechev Prefetch bundles which might be required in future No

    manual prefetching logic Uses link[rel=“prefetch|preload”]
  62. twitter.com/mgechev Prefetch bundles which might be required in future No

    manual prefetching logic Uses link[rel=“prefetch|preload”] Over-fetching & bandwidth consumption Does not allow prioritization of bundles Does not work with content
  63. twitter.com/mgechev

  64. twitter.com/mgechev slide by @addyosmani

  65. twitter.com/mgechev $ npm run eject $ vim webpack.config.js

  66. twitter.com/mgechev const { GuessPlugin } = require('guess-webpack'); !// !!... plugins:

    [ !// !!... new GuessPlugin({ GA: 'XXXXXXXXX' }) ] !// !!...
  67. twitter.com/mgechev /a /a/a /a/b /b /b/a /

  68. twitter.com/mgechev Markov Chain / /a /a/:id /b /b/a / 0

    0.3 0 0.7 0 /a 0 0 0.9 0.1 0 /a/:id 0 1 0 0 0 /b 0 0 0 0 1 /b/a 0 1 0 0 0 /a /a/a /a/b /b /b/a /
  69. twitter.com/mgechev /a /a/a /a/b /b /b/a / Activity: / /a

    /a/:id /b /b/a / 0 0.3 0 0.7 0 /a 0 0 0.9 0.1 0 /a/:id 0 1 0 0 0 /b 0 0 0 0 1 /b/a 0 1 0 0 0 Probability threshold: 0.5
  70. twitter.com/mgechev /a /a/a /a/b /b /b/a / Activity: / /a

    /a/:id /b /b/a / 0 0.3 0 0.7 0 /a 0 0 0.9 0.1 0 /a/:id 0 1 0 0 0 /b 0 0 0 0 1 /b/a 0 1 0 0 0 Probability threshold: 0.5
  71. twitter.com/mgechev /a /a/a /a/b /b /b/a / Activity: / /a

    /a/:id /b /b/a / 0 0.3 0 0.7 0 /a 0 0 0.9 0.1 0 /a/:id 0 1 0 0 0 /b 0 0 0 0 1 /b/a 0 1 0 0 0 Probability threshold: 0.5
  72. twitter.com/mgechev / /a /a/:id /b /b/a / 0 0.3 0

    0.7 0 /a 0 0 0.9 0.1 0 /a/:id 0 1 0 0 0 /b 0 0 0 0 1 /b/a 0 1 0 0 0 /a /a/a /a/b /b /b/a / Activity: - Download b.bundle.js Probability threshold: 0.5
  73. twitter.com/mgechev /a /a/a /a/b /b /b/a / Activity: - Download

    b.bundle.js / /a /a/:id /b /b/a / 0 0.3 0 0.7 0 /a 0 0 0.9 0.1 0 /a/:id 0 1 0 0 0 /b 0 0 0 0 1 /b/a 0 1 0 0 0 Probability threshold: 0.5
  74. twitter.com/mgechev /a /a/a /a/b /b /b/a / Activity: - Download

    b.bundle.js / /a /a/:id /b /b/a / 0 0.3 0 0.7 0 /a 0 0 0.9 0.1 0 /a/:id 0 1 0 0 0 /b 0 0 0 0 1 /b/a 0 1 0 0 0 Probability threshold: 0.5
  75. twitter.com/mgechev /a /a/a /a/b /b /b/a / / /a /a/:id

    /b /b/a / 0 0.3 0 0.7 0 /a 0 0 0.9 0.1 0 /a/:id 0 1 0 0 0 /b 0 0 0 0 1 /b/a 0 1 0 0 0 Activity: - Download b.bundle.js - No action Probability threshold: 0.5
  76. twitter.com/mgechev /a /a/a /a/b /b /b/a / / /a /a/:id

    /b /b/a / 0 0.3 0 0.7 0 /a 0 0 0.9 0.1 0 /a/:id 0 1 0 0 0 /b 0 0 0 0 1 /b/a 0 1 0 0 0 Activity: - Download b.bundle.js - No action Probability threshold: 0.5
  77. twitter.com/mgechev /a /a/a /a/b /b /b/a / / /a /a/:id

    /b /b/a / 0 0.3 0 0.7 0 /a 0 0 0.9 0.1 0 /a/:id 0 1 0 0 0 /b 0 0 0 0 1 /b/a 0 1 0 0 0 Activity: - Download b.bundle.js - No action Probability threshold: 0.5
  78. twitter.com/mgechev /a /a/a /a/b /b /b/a / / /a /a/:id

    /b /b/a / 0 0.3 0 0.7 0 /a 0 0 0.9 0.1 0 /a/:id 0 1 0 0 0 /b 0 0 0 0 1 /b/a 0 1 0 0 0 Activity: - Download b.bundle.js - No action - Download a.bundle.js Probability threshold: 0.5
  79. twitter.com/mgechev • https://mgv.io/guess • https://mgv.io/dd-bundling • https://mgv.io/cost-of-js • https://mgv.io/predictive-fetching Resources

  80. Runtime Performance

  81. twitter.com/mgechev Simplified Business application

  82. None
  83. AppComponent EmployeesListComponent

  84. AppComponent EmployeesListComponent

  85. AppComponent EmployeesListComponent

  86. <input [(ngModel)]="label" (keydown)="handleKey($event)"> <mat-list-item *ngFor="let item of data"> {{ item.label

    }} {{ calculate(item.num) }} !</mat-list-item>
  87. <input [(ngModel)]="label" (keydown)="handleKey($event)"> <mat-list-item *ngFor="let item of data"> {{ item.label

    }} {{ calculate(item.num) }} !</mat-list-item>
  88. <input [(ngModel)]="label" (keydown)="handleKey($event)"> <mat-list-item *ngFor="let item of data"> {{ item.label

    }} {{ calculate(item.num) }} !</mat-list-item>
  89. <input [(ngModel)]="label" (keydown)="handleKey($event)"> <mat-list-item *ngFor="let item of data"> {{ item.label

    }} {{ calculate(item.num) }} !</mat-list-item>
  90. @Component(!!...) export class EmployeeListComponent { @Input() data: EmployeeData[]; @Output() remove

    = new EventEmitter<EmployeeData>(); @Output() add = new EventEmitter<string>(); handleKey(event: any) { !!... } calculate(num: number) { return fibonacci(num); } }
  91. @Component(!!...) export class EmployeeListComponent { @Input() data: EmployeeData[]; @Output() remove

    = new EventEmitter<EmployeeData>(); @Output() add = new EventEmitter<string>(); handleKey(event: any) { !!... } calculate(num: number) { return fibonacci(num); } }
  92. AppComponent EmployeesListComponent data

  93. @Component(!!...) export class EmployeeListComponent { @Input() data: EmployeeData[]; @Output() remove

    = new EventEmitter<EmployeeData>(); @Output() add = new EventEmitter<string>(); handleKey(event: any) { !!... } calculate(num: number) { return fibonacci(num); } }
  94. const fibonacci = n !=> { if (n !!=== 1

    !|| n !!=== 2) return 1; return fibonacci(n - 1) + fibonacci(n - 2); };
  95. Slowing it down artificially

  96. twitter.com/mgechev Application Structure • An application component • Two list

    components • Slow computation for each entry
  97. twitter.com/mgechev Real data…

  98. None
  99. 140 entries

  100. None
  101. Why that slow?

  102. None
  103. AppComponent EmployeeListComponenet (sales) EmployeeListComponenet (r&d) E1 E2 En … E1

    E2 En … Ignored details for simplicity
  104. AppComponent EmployeeListComponenet (sales) EmployeeListComponenet (r&d) E1 E2 En … E1

    E2 En … Ignored details for simplicity
  105. AppComponent EmployeeListComponenet (sales) EmployeeListComponenet (r&d) E1 E2 En … E1

    E2 En … Ignored details for simplicity
  106. AppComponent EmployeeListComponenet (sales) EmployeeListComponenet (r&d) E1 E2 En … E1

    E2 En … Ignored details for simplicity
  107. AppComponent EmployeeListComponenet (sales) EmployeeListComponenet (r&d) E1 E2 En … E1

    E2 En … Ignored details for simplicity
  108. AppComponent EmployeeListComponenet (sales) EmployeeListComponenet (r&d) E1 E2 En … E1

    E2 En … Ignored details for simplicity
  109. AppComponent EmployeeListComponenet (sales) EmployeeListComponenet (r&d) E1 E2 En … E1

    E2 En … Ignored details for simplicity
  110. twitter.com/mgechev Ideas for optimization?

  111. twitter.com/mgechev OnPush Change Detection Strategy

  112. twitter.com/mgechev With OnPush change detection will be triggered when the

    framework, with reference check, determines that any of the inputs of a component has changed…
  113. twitter.com/mgechev Passing new reference triggers the change detection

  114. twitter.com/mgechev Should we copy the array every time?

  115. Why would we do that…?

  116. None
  117. twitter.com/mgechev Immutable.js helps: • We get a new reference on

    change • We do not copy the entire data structure
  118. twitter.com/mgechev Let’s see how fast it is now!

  119. None
  120. Non Optimized On Push Typing Speed

  121. ~3x faster but still slow…

  122. None
  123. twitter.com/mgechev With OnPush change detection will be triggered when the

    framework, with reference check, determines that any of the inputs of a component has changed…or when an event in the component is triggered
  124. twitter.com/mgechev Let’s do some refactoring!

  125. AppComponent EmployeesListComponent NameInputComponent ListComponent

  126. AppComponent EmployeesListComponent NameInputComponent ListComponent

  127. AppComponent EmployeesListComponent NameInputComponent ListComponent

  128. None
  129. twitter.com/mgechev Sooo much faster!

  130. AppComponent EmployeeListComponenet (sales) EmployeeListComponenet (r&d) E1 E2 En … E1

    E2 En … ListComponenet (sales) ListComponenet (r&d) Ignored details for simplicity
  131. AppComponent EmployeeListComponenet (sales) EmployeeListComponenet (r&d) E1 E2 En … E1

    E2 En … ListComponenet (sales) ListComponenet (r&d) Ignored details for simplicity
  132. AppComponent EmployeeListComponenet (sales) EmployeeListComponenet (r&d) E1 E2 En … E1

    E2 En … ListComponenet (sales) ListComponenet (r&d) Ignored details for simplicity
  133. AppComponent EmployeeListComponenet (sales) EmployeeListComponenet (r&d) E1 E2 En … E1

    E2 En … ListComponenet (sales) ListComponenet (r&d) Ignored details for simplicity
  134. Unoptimized Refactored with On Push Typing Speed

  135. Adding items

  136. twitter.com/mgechev Recomputing everything every time we add a new entry

  137. const fibonacci = n !=> { if (n !!=== 1

    !|| n !!=== 2) return 1; return fibonacci(n - 1) + fibonacci(n - 2); };
  138. // Two properties // - No side effects // -

    Same result for same arguments const fibonacci = n !=> { if (n !!=== 1 !|| n !!=== 2) return 1; return fibonacci(n - 1) + fibonacci(n - 2); };
  139. twitter.com/mgechev Pure Function

  140. twitter.com/mgechev Pipes in Angular • Pure • Impure

  141. {{ birthday | date }} {{ birthday | impureDate }}

  142. {{ birthday | date }} {{ birthday | impureDate }}

    i1.ɵunv(_v, 1, 0, _ck(_v, 2, 0, i1.ɵnov(_v, 0), _co.birthday)); i1.ɵunv(_v, 1, 1, i1.ɵnov(_v, 3).transform(_co.birthday));
  143. {{ birthday | date }} {{ birthday | impureDate }}

    i1.ɵunv(_v, 1, 0, _ck(_v, 2, 0, i1.ɵnov(_v, 0), _co.birthday)); i1.ɵunv(_v, 1, 1, i1.ɵnov(_v, 3).transform(_co.birthday));
  144. twitter.com/mgechev Angular executes a pure pipe only when it detects

    a change to the input value. A pure change is either a change to a primitive input value (String, Number, Boolean, Symbol) or a changed object reference (Date, Array, Function, Object).
  145. @Pipe({ name: 'calculate', pure: true }) export class CalculatePipe {

    transform(num: number) { return fibonacci(num); } }
  146. @Component({ changeDetection: ChangeDetectionStrategy.OnPush, template: ` !!... {{ item.num | calculate

    }} !!... ` }) export class ListComponent { !!... }
  147. twitter.com/mgechev Lets benchpress it!

  148. None
  149. Refactored with On Push Calculation in a Pure Pipe Adding

    / Removing Entries
  150. twitter.com/mgechev • Use OnPush in an isolated context • Consider

    immutable.js with OnPush • Use pure pipes instead of method calls Tips on runtime performance
  151. twitter.com/mgechev mgv.io/ng-cd - Angular’s OnPush Change Detection Strategy mgv.io/ng-pure -

    Pure Pipes and Referential Transparency mgv.io/ng-diff - Understanding Angular Differs mgv.io/ng-perf-checklist - Angular Performance Checklist mgv.io/ng-checklist-video - Angular Performance Checklist (video) Resources
  152. Thank you! twitter.com/mgechev github.com/mgechev blog.mgechev.com