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. In it’s second part, we’ll discuss different techniques which can improve the runtime performance of our application in order to help us achieve rendering with 60fps!

82bafb0432ce4ccc9dcc26f94d5fe5bc?s=128

Minko Gechev

May 30, 2017
Tweet

Transcript

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

  2. twitter.com/mgechev twitter.com/mgechev twitter.com/mgechev github.com/mgechev

  3. twitter.com/mgechev

  4. None
  5. None
  6. None
  7. None
  8. twitter.com/mgechev Runtime Performance

  9. twitter.com/mgechev Change Detection

  10. twitter.com/mgechev Mechanism for detecting changes which should be reflected in

    the view
  11. twitter.com/mgechev Techniques ‣ Observer pattern ‣ Virtual DOM ‣ Dirty

    checking
  12. twitter.com/mgechev

  13. twitter.com/mgechev Observer Pattern ‣ Push based ‣ State triggers an

    event on change ‣ View handles the event and updates
  14. // state.ts class State extends EventEmitter {...} const state =

    new State({ todos: [] }); state.todos.push('Buy milk'); state.emit(Change) // view.ts class View { // more code update(state) { // visualize todos } } const view = new View(); state.on(Change, _ => view.update(state));
  15. // state.ts class State extends EventEmitter {...} const state =

    new State({ todos: [] }); state.todos.push('Buy milk'); state.emit(Change) // view.ts class View { // more code update(state) { // visualize todos } } const view = new View(); state.on(Change, _ => view.update(state));
  16. twitter.com/mgechev Virtual Dom ‣ State renders in a “virtual view”

    ‣ Diff between the last two versions of the “virtual view” ‣ Diff applied to the view
  17. [] State TODOS APP INPUT BUTTON

  18. TODOS APP INPUT BUTTON State ['Buy milk']

  19. State ['Buy milk'] TODOS APP INPUT BUTTON TODOS APP INPUT

    BUTTON TODO
  20. State ['Buy milk'] TODOS APP INPUT BUTTON TODOS APP INPUT

    BUTTON TODO
  21. State ['Buy milk'] TODOS APP INPUT BUTTON TODO

  22. State ['Buy milk'] TODOS APP INPUT BUTTON TODO

  23. twitter.com/mgechev Dirty Checking ‣ Pull based ‣ Framework checks for

    state updates ‣ Framework update the view
  24. twitter.com/mgechev Dirty Checking ‣ Pull based ‣ Framework checks for

    state updates ‣ Framework update the view
  25. twitter.com/mgechev When?

  26. twitter.com/mgechev Dirty Checking ‣ After each event callback ‣ After

    each network message ‣ After each timeout callback ‣ …
  27. sample.ts fetch(url) .then(r => r.json()) .then(data => { // mutate

    the state detectChanges(); }); timeout(() => { // mutate the state detectChanges(); }, 1000); btn.addEventListener('click', () => { // mutate the state detectChanges(); }); const detectChanges = _ => { if (state !== prevState) { updateView(state); } };
  28. sample.ts fetch(url) .then(r => r.json()) .then(data => { // mutate

    the state detectChanges(); }); timeout(() => { // mutate the state detectChanges(); }, 1000); btn.addEventListener('click', () => { // mutate the state detectChanges(); }); const detectChanges = _ => { if (state !== prevState) { updateView(state); } };
  29. sample.ts fetch(url) .then(r => r.json()) .then(data => { // mutate

    the state detectChanges(); }); timeout(() => { // mutate the state detectChanges(); }, 1000); btn.addEventListener('click', () => { // mutate the state detectChanges(); }); const detectChanges = _ => { if (state !== prevState) { updateView(state); } };
  30. sample.ts fetch(url) .then(r => r.json()) .then(data => { // mutate

    the state detectChanges(); }); timeout(() => { // mutate the state detectChanges(); }, 1000); btn.addEventListener('click', () => { // mutate the state detectChanges(); }); const detectChanges = _ => { if (state !== prevState) { updateView(state); } };
  31. sample.ts fetch(url) .then(r => r.json()) .then(data => { // mutate

    the state detectChanges(); }); timeout(() => { // mutate the state detectChanges(); }, 1000); btn.addEventListener('click', () => { // mutate the state detectChanges(); }); const detectChanges = _ => { if (state !== prevState) { updateView(state); } };
  32. twitter.com/mgechev Zone.js intercepts all tasks and micro tasks

  33. zone.ts (pseudo code) https://goo.gl/j1Osvu const orig = Node.prototype.addEventListener; Node.prototype.addEventListener =

    function (old, f) { let cb = () => { old(); zone.afterInvoke(); }; orig.call(this, cb, f); };
  34. ... this._zone.onMicrotaskEmpty.subscribe( { next: () => { this._zone.run(() => {

    this.tick(); }); } } ); ... application_ref.ts
  35. application_ref.ts ... tick(): void { ... try { this._views.forEach((view) =>

    view.detectChanges()); if (this._enforceNoNewChanges) { this._views.forEach((view) => view.checkNoChanges()); } } catch (e) { ... } } ...
  36. application_ref.ts ... tick(): void { ... try { this._views.forEach((view) =>

    view.detectChanges()); if (this._enforceNoNewChanges) { this._views.forEach((view) => view.checkNoChanges()); } } catch (e) { ... } } ...
  37. twitter.com/mgechev APP TODOS TODO INPUT BUTTON

  38. twitter.com/mgechev APP TODOS TODO INPUT BUTTON

  39. twitter.com/mgechev APP TODOS TODO INPUT BUTTON

  40. twitter.com/mgechev APP TODOS TODO INPUT BUTTON

  41. twitter.com/mgechev APP TODOS TODO INPUT BUTTON

  42. twitter.com/mgechev APP TODOS TODO INPUT BUTTON

  43. ... <input [(ngModel)]="newTodo"> ... <todo-cmp *ngFor="let todo of todos" [todo]="spellCheck(todo)">

    </todo-cmp> ...
  44. ... <input [(ngModel)]="newTodo"> ... <todo-cmp *ngFor="let todo of todos" [todo]="spellCheck(todo)">

    </todo-cmp> ...
  45. ... <input [(ngModel)]="newTodo"> ... <todo-cmp *ngFor="let todo of todos" [todo]="spellCheck(todo)">

    </todo-cmp> ...
  46. twitter.com/mgechev We have less than 16.667ms for JavaScript execution

  47. twitter.com/mgechev Do not add bindings to heavy computations in the

    templates Practice #1
  48. application_ref.ts ... tick(): void { ... try { this._views.forEach((view) =>

    view.detectChanges()); if (this._enforceNoNewChanges) { this._views.forEach((view) => view.checkNoChanges()); } } catch (e) { ... } } ...
  49. circular-update.ts @Component({ selector: 'app-counter', template: '{{counter}}' }) export class CounterComponent

    { private _counter = 1; get counter() { return this._counter++; } }
  50. circular-update.ts @Component({ selector: 'app-counter', template: '{{counter}}' }) export class CounterComponent

    { private _counter = 1; get counter() { return this._counter++; } }
  51. twitter.com/mgechev view.checkNoChanges slows us down in production, at least twice!

  52. twitter.com/mgechev Always invoke enableProdMode in our production applications Default behavior

    for the production builds of Angular CLI, angular seed Practice #2
  53. twitter.com/mgechev How Angular Updates The View ‣ Checks for changes

    in the state ‣ Applies only required updates
  54. twitter.com/mgechev Angular generates code for performing change detection and updating

    the view
  55. twitter.com/mgechev Generation Could Be ‣ Runtime, a.k.a. Just-in-Time ‣ Build

    time, a.k.a. Ahead-of-Time
  56. None
  57. twitter.com/mgechev Performing the compilation step runtime (JiT) slows down the

    initial rendering
  58. twitter.com/mgechev Use Ahead-of-Time compilation in production Default behavior for the

    production builds of Angular CLI, angular seed Practice #3
  59. twitter.com/mgechev Use Ahead-of-Time compilation in production Default behavior for the

    production builds of Angular CLI, angular seed Practice #3 everywhere
  60. twitter.com/mgechev The best way to speed-up the change detection is

    to not perform it at all!
  61. twitter.com/mgechev ChangeDetectorRef

  62. twitter.com/mgechev ChangeDetectorRef ‣ Detach the change detector ‣ Check for

    changes manually ‣ Re-attach the change detector Allows us to:
  63. @Component({ selector: 'todos-cmp', template: ` <todo *ngFor="let item of items"

    [todo]="item"> </todo> ` }) class TodosComponent { items: Todo[] = []; constructor(private ref: ChangeDetectorRef) { ref.detach(); } addItem(item: Todo) { this.items.push(item); this.ref.detectChanges(); } }
  64. @Component({ selector: 'todos-cmp', template: ` <todo *ngFor="let item of items"

    [todo]="item"> </todo> ` }) class TodosComponent { items: Todo[] = []; constructor(private ref: ChangeDetectorRef) { ref.detach(); } addItem(item: Todo) { this.items.push(item); this.ref.detectChanges(); } }
  65. @Component({ selector: 'todos-cmp', template: ` <todo *ngFor="let item of items"

    [todo]="item"> </todo> ` }) class TodosComponent { items: Todo[] = []; constructor(private ref: ChangeDetectorRef) { ref.detach(); } addItem(item: Todo) { this.items.push(item); this.ref.detectChanges(); } }
  66. twitter.com/mgechev APP TODOS TODO INPUT BUTTON

  67. twitter.com/mgechev APP TODOS TODO INPUT BUTTON

  68. twitter.com/mgechev APP TODOS TODO INPUT BUTTON

  69. twitter.com/mgechev APP TODOS TODO INPUT BUTTON

  70. twitter.com/mgechev APP TODOS TODO INPUT BUTTON todos.addItem(todo);

  71. twitter.com/mgechev APP TODOS TODO INPUT BUTTON todos.addItem(todo);

  72. twitter.com/mgechev APP TODOS TODO INPUT BUTTON TODO todos.addItem(todo);

  73. twitter.com/mgechev Useful when… ‣ Show multiple updates in batch ‣

    Implement custom change detection logic
  74. twitter.com/mgechev Consider manual change detection invocation in case of large

    state or frequent updates Practice #4
  75. twitter.com/mgechev Externalizing the state

  76. @Component({ selector: 'todos-cmp', template: `...` }) class TodosComponent { _items:

    Todo[] = []; constructor(private ref: ChangeDetectorRef) { ref.detach(); } @Input() set items(items: Todo[]) { this._items = items; this.ref.detectChanges(); } get items() { return this._items; } }
  77. @Component({ selector: 'todos-cmp', template: `...` }) class TodosComponent { _items:

    Todo[] = []; constructor(private ref: ChangeDetectorRef) { ref.detach(); } @Input() set items(items: Todo[]) { this._items = items; this.ref.detectChanges(); } get items() { return this._items; } }
  78. @Component({ selector: 'todos-cmp', template: `...` }) class TodosComponent { _items:

    Todo[] = []; constructor(private ref: ChangeDetectorRef) { ref.detach(); } @Input() set items(items: Todo[]) { this._items = items; this.ref.detectChanges(); } get items() { return this._items; } }
  79. twitter.com/mgechev Two problems… ‣ Variable could be mutated by other

    component Change detection won’t work
  80. twitter.com/mgechev ParentComponent SiblingComponent TodosComponent const todos = [a, b, c];

  81. twitter.com/mgechev ParentComponent SiblingComponent TodosComponent const todos = [a, b, c];

    todos
  82. twitter.com/mgechev ParentComponent SiblingComponent TodosComponent const todos = [a, b, c];

    todos todos todos.push(d);
  83. twitter.com/mgechev ParentComponent SiblingComponent TodosComponent const todos = [a, b, c];

    todos todos todos.push(d);
  84. twitter.com/mgechev Two problems… ‣ Variable could be mutated by other

    component ‣ Change detection won’t work
  85. twitter.com/mgechev ParentComponent SiblingComponent TodosComponent const todos = [a, b, c];

    todos.slice();
  86. twitter.com/mgechev Immutability

  87. @Component({ selector: 'todos-cmp', template: `...` changeDetection: ChangeDetectionStrategy.OnPush }) class TodosComponent

    { _items: Todo[] = []; constructor(private ref: ChangeDetectorRef) { ref.detach(); } @Input() set items(items: Todo[]) { this._items = items; this.ref.detectChanges(); } get items() { return this._items; } }
  88. @Component({ selector: 'todos-cmp', template: `...` changeDetection: ChangeDetectionStrategy.OnPush }) class TodosComponent

    { _items: Todo[] = []; constructor(private ref: ChangeDetectorRef) { ref.detach(); } @Input() set items(items: Todo[]) { this._items = items; this.ref.detectChanges(); } get items() { return this._items; } }
  89. @Component({ selector: 'todos-cmp', template: `...`, changeDetection: ChangeDetectionStrategy.OnPush }) class TodosComponent

    { _items: Todo[] = []; constructor(private ref: ChangeDetectorRef) { ref.detach(); } @Input() set items(items: Todo[]) { this._items = items; this.ref.detectChanges(); } get items() { return this._items; } }
  90. twitter.com/mgechev Externalize the state and use OnPush for skipping CD

    for component subtrees Practice #5
  91. twitter.com/mgechev Pipes

  92. twitter.com/mgechev Memoization is a technique used to speed up programs

    by storing the results of function calls and returning the cached result when the same inputs occur again.
  93. twitter.com/mgechev Pure Functions ‣ Same result when invoked with same

    arguments ‣ Do not have side-effects Can be optimized with memoization because:
  94. const fib = n => { if (n === 1

    || n === 2) return 1; return fib(n - 1) + fib(n - 2); } @Pipe({ name: 'fib', pure: true }) export FibPipe implements PipeTransform { transform(val: number) { return fib(val); } }
  95. const fib = n => { if (n === 1

    || n === 2) return 1; return fib(n - 1) + fib(n - 2); } @Pipe({ name: 'fib', pure: true }) export FibPipe implements PipeTransform { transform(val: number) { return fib(val); } }
  96. const fib = n => { if (n === 1

    || n === 2) return 1; return fib(n - 1) + fib(n - 2); } @Pipe({ name: 'fib', pure: true }) export FibPipe implements PipeTransform { transform(val: number) { return fib(val); } }
  97. twitter.com/mgechev Use pure pipes for faster expression evaluation Practice #6

  98. twitter.com/mgechev mgv.io/ng-perf-checklist

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