Purely Fast

82bafb0432ce4ccc9dcc26f94d5fe5bc?s=47 Minko Gechev
November 07, 2017

Purely Fast

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 together 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.

Links:
https://github.com/mgechev/purely-fast
https://github.com/mgechev/purely-fast-benchmarks

82bafb0432ce4ccc9dcc26f94d5fe5bc?s=128

Minko Gechev

November 07, 2017
Tweet

Transcript

  1. twitter.com/mgechev github.com/mgechev blog.mgechev.com Purely Fast By Minko Gechev

  2. None
  3. twitter.com/mgechev github.com/mgechev

  4. Runtime Performance

  5. twitter.com/mgechev Simplified Business application

  6. None
  7. AppComponent EmployeesListComponent

  8. AppComponent EmployeesListComponent

  9. AppComponent EmployeesListComponent

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

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

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

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

    }} {{ calculate(item.num) }} </mat-list-item>
  14. @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); } }
  15. @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); } }
  16. AppComponent EmployeesListComponent data

  17. @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); } }
  18. const fibonacci = n => { if (n === 1

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

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

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

  22. None
  23. 140 entries

  24. None
  25. Why that slow?

  26. @Component(...) export class EmployeeListComponent { ... calculate(num: number) { console.log('Computing

    for entry in', this.department); return fibonacci(num); } }
  27. None
  28. twitter.com/mgechev Ideas for optimization?

  29. twitter.com/mgechev OnPush Change Detection Strategy

  30. 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…
  31. twitter.com/mgechev Lets think of EmployeeListComponent as a function, where: What

    does this mean? • Inputs are function’s arguments • Rendered component is function’s result
  32. const f = EmployeeListComponent; const data = [e1]; // Will

    trigger CD f({ data: data }); data.push(e2); // Will not trigger CD f({ data: data }); // Will trigger CD f({ data: data.slice() }); Pseudo code (not Angular)
  33. const f = EmployeeListComponent; const data = [e1]; // Will

    trigger CD f({ data: data }); data.push(e2); // Will not trigger CD f({ data: data }); // Will trigger CD f({ data: data.slice() }); Pseudo code (not Angular)
  34. const f = EmployeeListComponent; const data = [e1]; // Will

    trigger CD f({ data: data }); data.push(e2); // Will not trigger CD f({ data: data }); // Will trigger CD f({ data: data.slice() }); Pseudo code (not Angular)
  35. const f = EmployeeListComponent; const data = [e1]; // Will

    trigger CD f({ data: data }); data.push(e2); // Will not trigger CD f({ data: data }); // Will trigger CD f({ data: data.slice() }); Pseudo code (not Angular)
  36. twitter.com/mgechev Passing new reference triggers the change detection

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

  38. Why would we do that…?

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

    change • We do not copy the entire data structure
  41. twitter.com/mgechev Lets see how fast it is now!

  42. None
  43. Unoptimized On Push Typing Speed

  44. 2x faster but still slow…

  45. None
  46. 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
  47. twitter.com/mgechev Lets do some refactoring!

  48. AppComponent EmployeesListComponent NameInputComponent ListComponent

  49. AppComponent EmployeesListComponent NameInputComponent ListComponent

  50. AppComponent EmployeesListComponent NameInputComponent ListComponent

  51. None
  52. twitter.com/mgechev Sooo much faster!

  53. Unoptimized Refactored with On Push Typing Speed

  54. Adding items

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

  56. const fibonacci = n => { if (n === 1

    || n === 2) return 1; return fibonacci(n - 1) + fibonacci(n - 2); };
  57. const fibonacci = n => { if (n === 1

    || n === 2) return 1; return fibonacci(n - 1) + fibonacci(n - 2); }; // Two properties // - No side effects // - Same result for same arguments
  58. twitter.com/mgechev Pure Function

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

  60. 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).
  61. @Pipe({ name: 'calculate', pure: true }) export class CalculatePipe {

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

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

  64. None
  65. Refactored with On Push Calculation in a Pure Pipe Adding

    / Removing Entries
  66. twitter.com/mgechev Initial rendering… with 1000 items

  67. None
  68. None
  69. twitter.com/mgechev Lets take a look at our data

  70. None
  71. fibonacci(27)

  72. fibonacci(27) fibonacci(28)

  73. fibonacci(27) fibonacci(28) fibonacci(29)

  74. twitter.com/mgechev Samples from a small range During initial rendering we

    recompute same value multiple times
  75. twitter.com/mgechev Solution Caching the value once computed

  76. twitter.com/mgechev Memoization

  77. twitter.com/mgechev Memoization possible for pure functions

  78. const memoize = require('lodash.memoize'); const fibonacci = memoize((num: number): number

    => { if (num === 1 || num === 2) return 1; return fibonacci(num - 1) + fibonacci(num - 2); });
  79. const memoize = require('lodash.memoize'); const fibonacci = memoize((num: number): number

    => { if (num === 1 || num === 2) return 1; return fibonacci(num - 1) + fibonacci(num - 2); });
  80. None
  81. None
  82. Pure Pipe Pure Pipe & Memoization Initial Rendering

  83. Pure Pipes 27 | calculate 27 | calculate 27 |

    calculate
  84. Pure Pipes 27 | calculate 27 | calculate 27 |

    calculate fib(27) 196418
  85. Pure Pipes 27 | calculate 27 | calculate 27 |

    calculate fib(27) fib(27) 196418 196418
  86. Pure Pipes 27 | calculate 27 | calculate 27 |

    calculate fib(27) fib(27) fib(27) 196418 196418 196418
  87. Memoization 27 | calculate 27 | calculate 27 | calculate

  88. 27 | calculate 27 | calculate 27 | calculate fib(27)

    196418 Memoization
  89. 27 | calculate 27 | calculate 27 | calculate fib(27)

    196418 cache Memoization 27
  90. 27 | calculate 27 | calculate 27 | calculate fib(27)

    196418 cache fib(27) Memoization 27 27
  91. 27 | calculate 27 | calculate 27 | calculate fib(27)

    196418 cache fib(27) 196418 Memoization 27 27
  92. 27 | calculate 27 | calculate 27 | calculate fib(27)

    196418 cache fib(27) 196418 fib(27) Memoization 27 27 27
  93. 27 | calculate 27 | calculate 27 | calculate fib(27)

    196418 cache fib(27) 196418 fib(27) 196418 Memoization 27 27 27
  94. twitter.com/mgechev Pattern… • On push performs “memoization” • Pure pipes

    are “memoized”
  95. twitter.com/mgechev Pattern… for their last input • On push performs

    “memoization” • Pure pipes are “memoized”
  96. twitter.com/mgechev Lets try to do better!

  97. @Directive({selector: '[ngFor][ngForOf]'}) export class NgForOf<T> implements DoCheck, OnChanges { ...

    constructor(private _differs: IterableDiffers) {} ngDoCheck(): void { const changes = this._differ.diff(this.ngForOf); if (changes) this._applyChanges(changes); } ... } How NgForOf works
  98. @Directive({selector: '[ngFor][ngForOf]'}) export class NgForOf<T> implements DoCheck, OnChanges { ...

    constructor(private _differs: IterableDiffers) {} ngDoCheck(): void { const changes = this._differ.diff(this.ngForOf); if (changes) this._applyChanges(changes); } ... } How NgForOf works
  99. @Directive({selector: '[ngFor][ngForOf]'}) export class NgForOf<T> implements DoCheck, OnChanges { ...

    constructor(private _differs: IterableDiffers) {} ngDoCheck(): void { const changes = this._differ.diff(this.ngForOf); if (changes) this._applyChanges(changes); } ... } How NgForOf works
  100. twitter.com/mgechev IterableDiffer checks whether the data structure has changed

  101. twitter.com/mgechev But the data structure knows that best!

  102. export class DifferableList<T> { changes = new LinkedList<IterableChangeRecord<T>>(); constructor(private data

    = List<T>([])) {} unshift(data: T) { const result = new DifferableList<T>(this.data.unshift(data)); result.changes.add({ ... }); return result; } ... [Symbol.iterator]() { return new DifferableListIterator<T>(this); } }
  103. export class DifferableList<T> { changes = new LinkedList<IterableChangeRecord<T>>(); constructor(private data

    = List<T>([])) {} unshift(data: T) { const result = new DifferableList<T>(this.data.unshift(data)); result.changes.add({ ... }); return result; } ... [Symbol.iterator]() { return new DifferableListIterator<T>(this); } }
  104. export class DifferableList<T> { changes = new LinkedList<IterableChangeRecord<T>>(); constructor(private data

    = List<T>([])) {} unshift(data: T) { const result = new DifferableList<T>(this.data.unshift(data)); result.changes.add({ ... }); return result; } ... [Symbol.iterator]() { return new DifferableListIterator<T>(this); } }
  105. export class DifferableList<T> { changes = new LinkedList<IterableChangeRecord<T>>(); constructor(private data

    = List<T>([])) {} unshift(data: T) { const result = new DifferableList<T>(this.data.unshift(data)); result.changes.add({ ... }); return result; } ... [Symbol.iterator]() { return new DifferableListIterator<T>(this); } }
  106. export class DifferableList<T> { changes = new LinkedList<IterableChangeRecord<T>>(); constructor(private data

    = List<T>([])) {} unshift(data: T) { const result = new DifferableList<T>(this.data.unshift(data)); result.changes.add({ ... }); return result; } ... [Symbol.iterator]() { return new DifferableListIterator<T>(this); } }
  107. twitter.com/mgechev Data structure optimized for Angular

  108. export class DifferableListDiffer<V> implements IterableDiffer<V>, IterableChanges<V> { ... diff(collection: NgIterable<V>):

    DifferableListDiffer<V> | null { const changes = this._data.changes; this._changes = changes; if (changes.size() > 0) { this._data.changes = new LinkedList<IterableChangeRecord<V>>(); return this; } else { return null; } } }
  109. export class DifferableListDiffer<V> implements IterableDiffer<V>, IterableChanges<V> { ... diff(collection: NgIterable<V>):

    DifferableListDiffer<V> | null { const changes = this._data.changes; this._changes = changes; if (changes.size() > 0) { this._data.changes = new LinkedList<IterableChangeRecord<V>>(); return this; } else { return null; } } }
  110. twitter.com/mgechev Persistent data structures Inspired by

  111. Default Differ Custom Differ Adding / Removing Entries

  112. twitter.com/mgechev Lessons learned • No silver bullet • Understand your

    component structure • Understand your data • Application specific benchmarks
  113. twitter.com/mgechev Get inspiration from computer science

  114. EBSTATD40 40% OFF

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