Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Purely Fast

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

Minko Gechev

November 07, 2017
Tweet

More Decks by Minko Gechev

Other Decks in Programming

Transcript

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

    View Slide

  2. View Slide

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

    View Slide

  4. Runtime Performance

    View Slide

  5. twitter.com/mgechev
    Simplified
    Business application

    View Slide

  6. View Slide

  7. AppComponent
    EmployeesListComponent

    View Slide

  8. AppComponent
    EmployeesListComponent

    View Slide

  9. AppComponent
    EmployeesListComponent

    View Slide

  10. (keydown)="handleKey($event)">
    *ngFor="let item of data”>
    {{ item.label }}
    {{ calculate(item.num) }}

    View Slide

  11. (keydown)="handleKey($event)">
    *ngFor="let item of data”>
    {{ item.label }}
    {{ calculate(item.num) }}

    View Slide

  12. (keydown)="handleKey($event)">
    *ngFor="let item of data”>
    {{ item.label }}
    {{ calculate(item.num) }}

    View Slide

  13. (keydown)="handleKey($event)">
    *ngFor="let item of data”>
    {{ item.label }}
    {{ calculate(item.num) }}

    View Slide

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

    View Slide

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

    View Slide

  16. AppComponent
    EmployeesListComponent
    data

    View Slide

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

    View Slide

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

    View Slide

  19. Slowing it down artificially

    View Slide

  20. twitter.com/mgechev
    Application Structure
    • An application component
    • Two list components
    • Slow computation for each entry

    View Slide

  21. twitter.com/mgechev
    Real data…

    View Slide

  22. View Slide

  23. 140 entries

    View Slide

  24. View Slide

  25. Why that slow?

    View Slide

  26. @Component(...)
    export class EmployeeListComponent {
    ...
    calculate(num: number) {
    console.log('Computing for entry in',
    this.department);
    return fibonacci(num);
    }
    }

    View Slide

  27. View Slide

  28. twitter.com/mgechev
    Ideas for optimization?

    View Slide

  29. twitter.com/mgechev
    OnPush
    Change Detection Strategy

    View Slide

  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…

    View Slide

  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

    View Slide

  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)

    View Slide

  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)

    View Slide

  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)

    View Slide

  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)

    View Slide

  36. twitter.com/mgechev
    Passing new reference
    triggers the change detection

    View Slide

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

    View Slide

  38. Why would we do that…?

    View Slide

  39. View Slide

  40. twitter.com/mgechev
    Immutable.js helps:
    • We get a new reference on change
    • We do not copy the entire data structure

    View Slide

  41. twitter.com/mgechev
    Lets see how fast
    it is now!

    View Slide

  42. View Slide

  43. Unoptimized On Push
    Typing Speed

    View Slide

  44. 2x faster but still slow…

    View Slide

  45. View Slide

  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

    View Slide

  47. twitter.com/mgechev
    Lets do some refactoring!

    View Slide

  48. AppComponent
    EmployeesListComponent
    NameInputComponent ListComponent

    View Slide

  49. AppComponent
    EmployeesListComponent
    NameInputComponent ListComponent

    View Slide

  50. AppComponent
    EmployeesListComponent
    NameInputComponent ListComponent

    View Slide

  51. View Slide

  52. twitter.com/mgechev
    Sooo much faster!

    View Slide

  53. Unoptimized Refactored with On Push
    Typing Speed

    View Slide

  54. Adding items

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  58. twitter.com/mgechev
    Pure Function

    View Slide

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

    View Slide

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

    View Slide

  61. @Pipe({ name: 'calculate', pure: true })
    export class CalculatePipe {
    transform(num: number) {
    return fibonacci(num);
    }
    }

    View Slide

  62. @Component({
    changeDetection: ChangeDetectionStrategy.OnPush,
    template: `
    ...
    {{ item.num | calculate }}
    ...
    `
    })
    export class ListComponent { ... }

    View Slide

  63. twitter.com/mgechev
    Lets benchpress it!

    View Slide

  64. View Slide

  65. Refactored with On Push Calculation in a Pure Pipe
    Adding / Removing Entries

    View Slide

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

    View Slide

  67. View Slide

  68. View Slide

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

    View Slide

  70. View Slide

  71. fibonacci(27)

    View Slide

  72. fibonacci(27)
    fibonacci(28)

    View Slide

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

    View Slide

  74. twitter.com/mgechev
    Samples from a small range
    During initial rendering we recompute
    same value multiple times

    View Slide

  75. twitter.com/mgechev
    Solution
    Caching the value once computed

    View Slide

  76. twitter.com/mgechev
    Memoization

    View Slide

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

    View Slide

  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);
    });

    View Slide

  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);
    });

    View Slide

  80. View Slide

  81. View Slide

  82. Pure Pipe Pure Pipe & Memoization
    Initial Rendering

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  87. Memoization
    27 | calculate
    27 | calculate
    27 | calculate

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  94. twitter.com/mgechev
    Pattern…
    • On push performs “memoization”
    • Pure pipes are “memoized”

    View Slide

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

    View Slide

  96. twitter.com/mgechev
    Lets try to do better!

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  100. twitter.com/mgechev
    IterableDiffer checks
    whether the data structure has
    changed

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  107. twitter.com/mgechev
    Data structure
    optimized for Angular

    View Slide

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

    View Slide

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

    View Slide

  110. twitter.com/mgechev
    Persistent
    data structures
    Inspired by

    View Slide

  111. Default Differ Custom Differ
    Adding / Removing Entries

    View Slide

  112. twitter.com/mgechev
    Lessons learned
    • No silver bullet
    • Understand your component structure
    • Understand your data
    • Application specific benchmarks

    View Slide

  113. twitter.com/mgechev
    Get inspiration from
    computer science

    View Slide

  114. EBSTATD40
    40% OFF

    View Slide

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

    View Slide