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

Bringing Immutability to Angular

Bringing Immutability to Angular

Some ideas from the functional paradigm, such as pure functions and immutable data, constantly find broader applications in building user interface.

One of the biggest showdowns in an AngularJS application is caused by the evaluation of hundreds of expressions inside the $digest loop. Using immutable collections can help us reduce the running time of some of them. The unidirectional data flow in Angular 2 allows us to take advantage of the “pure components” for further optimizations in our applications.

In this talk I’m going to make an introduction to how we can take advantage of immutable data structures in our Angular 1 and 2 applications.

Minko Gechev

May 07, 2015
Tweet

More Decks by Minko Gechev

Other Decks in Programming

Transcript

  1. Bringing Immutability to Angular
    Minko Gechev
    github.com/mgechev
    twitter.com/mgechev
    blog.mgechev.com

    View Slide

  2. Minko Gechev
    github.com/mgechev
    twitter.com/mgechev
    {
    "job": "Freelancer",
    "hobbies": [
    "open source",
    "blogging",
    "teaching",
    "sports"
    ],
    "communityEvents": [
    "SofiaJS",
    "BeerJS Sofia"
    ]
    }

    View Slide

  3. Optimization of the $digest
    loop
    Hidden subtopic:

    View Slide

  4. How to optimize collections’
    watchers?

    View Slide

  5. $watch vs $watchCollection
    https://www.flickr.com/photos/[email protected]/12867045203/

    View Slide

  6. $watch
    $scope.$watch(expr, fn, deepCheck);

    View Slide

  7. $watch
    // ...
    // Most common watches are on primitives, in which case we can short
    // circuit it with === operator, only when === fails do we use .equals
    if (watch) {
    if ((value = watch.get(current)) !== (last = watch.last) &&
    !(watch.eq
    ? equals(value, last)
    : (typeof value === 'number' && typeof last === 'number'
    && isNaN(value) && isNaN(last)))) {
    // ...

    View Slide

  8. // ...
    // Most common watches are on primitives, in which case we can short
    // circuit it with === operator, only when === fails do we use .equals
    if (watch) {
    if ((value = watch.get(current)) !== (last = watch.last) &&
    !(watch.eq
    ? equals(value, last)
    : (typeof value === 'number' && typeof last === 'number'
    && isNaN(value) && isNaN(last)))) {
    // ...
    $watch
    could be slow…

    View Slide

  9. // ...
    // Most common watches are on primitives, in which case we can short
    // circuit it with === operator, only when === fails do we use .equals
    if (watch) {
    if ((value = watch.get(current)) !== (last = watch.last) &&
    !(watch.eq
    ? equals(value, last)
    : (typeof value === 'number' && typeof last === 'number'
    && isNaN(value) && isNaN(last)))) {
    // ...
    $watch
    or much faster…

    View Slide

  10. $watchCollection
    $scope.$watchCollection(expr, fn);

    View Slide

  11. $watchCollection
    // ...
    for (var i = 0; i < newLength; i++) {
    oldItem = oldValue[i];
    newItem = newValue[i];
    bothNaN = (oldItem !== oldItem) && (newItem !== newItem);
    if (!bothNaN && (oldItem !== newItem)) {
    changeDetected++;
    oldValue[i] = newItem;
    }
    }
    // ...

    View Slide

  12. $watchCollection
    // ...
    for (var i = 0; i < newLength; i++) {
    oldItem = oldValue[i];
    newItem = newValue[i];
    bothNaN = (oldItem !== oldItem) && (newItem !== newItem);
    if (!bothNaN && (oldItem !== newItem)) {
    changeDetected++;
    oldValue[i] = newItem;
    }
    }
    // ...
    could be slow…

    View Slide

  13. Two optimization options
    • Use $watch(expr,  fn,  false)
    • Change the reference, i.e. creation of a new collection
    • Use $watchCollection(expr,  fn)  
    • Decrease the iterations (possibly to one)

    View Slide

  14. • Use $watch(expr,  fn,  false)
    • Change the reference, i.e. creation of a new collection
    • Use $watchCollection(expr,  fn)  
    • Decrease the iterations (possibly to one)
    Two optimization options

    View Slide

  15. Immutable Data Structures

    View Slide

  16. – Wikipedia
    “In object-oriented and functional programming, an
    immutable object is an object whose state cannot be
    modified after it is created.”

    View Slide

  17. View Slide

  18. immutable-demo.js
    let list = Immutable.List([1, 2, 3]);
    let changed = list.push(4);
    console.log(changed === list); // false
    console.log(list); // [ 1, 2, 3 ]
    console.log(changed); // [ 1, 2, 3, 4 ]

    View Slide

  19. View Slide

  20. Main use cases
    • Concurrent data access
    • Avoid side effects

    View Slide

  21. How can we take advantage
    of this in AngularJS?

    View Slide

  22. When immutable collection
    “changes”, we get a new reference

    View Slide

  23. $scope.$watch(expression, function () {
    // Handle the change
    }, false);

    View Slide

  24. View Slide

  25. View Slide

  26. Increase the memory usage?
    Maintain the persistent data structure

    View Slide

  27. What a persistent
    data structure is?

    View Slide

  28. Smarter way of implementing
    immutable collections

    View Slide

  29. Let’s benchpress it!

    View Slide

  30. The benchmark…
    Main parameters:
    • Collection Type (immutable or standard)
    • $watch  
    • $watchCollection  
    • Collection Size between 5 and 10,000
    • Bindings Count between 5 and 100

    View Slide

  31. View Slide

  32. Let’s find the winner!
    https://www.flickr.com/photos/kiyoungphoto/15657162511/

    View Slide

  33. 5 items
    Script Running Time
    Bindings Count

    View Slide

  34. 100 items
    Script Running Time
    Bindings Count

    View Slide

  35. 500 items
    Script Running Time
    Bindings Count

    View Slide

  36. 10,000 items
    Script Running Time
    Bindings Count

    View Slide

  37. Why?
    Script Running Time
    Bindings Count

    View Slide

  38. iterates n times over all elements of the results
    vs
    iterates n times over the results’ references
    $watchCollection
    $watch(..,.., false)

    View Slide

  39. View Slide

  40. Caption
    Garbage Collection Time

    View Slide

  41. 10,000 items
    Garbage Collection Time
    Bindings Count

    View Slide

  42. What slows us down?
    • Garbage collection
    • Maintaining the persistent data structure on change

    View Slide

  43. Lets take one step
    backwards…

    View Slide

  44. • Use $watch(expr,  fn,  false)
    • Change the reference, i.e. creation of a new collection
    • Use $watchCollection(expr,  fn)  
    • Decrease the iterations (possibly to one)
    Two optimization options

    View Slide

  45. Two optimization options
    • Use $watch(expr,  fn,  false)
    • Change the reference, i.e. creation of a new collection
    • Use $watchCollection(expr,  fn)  
    • Decrease the iterations (possibly to one)

    View Slide

  46. VersionableList
    function VersionableList(list) {
    'use strict’;
    this._version = 0;
    defineProperty(this, '_data', {
    enumerable: false,
    value: list || []
    });
    }
    'push pop shift unshift'.split(' ')
    .forEach(function (key) {
    'use strict';
    defineMethod(VersionableList.prototype, key, function () {
    this._data[key].apply(this._data, arguments);
    this._updateVersion();
    });
    });

    View Slide

  47. VersionableList
    function VersionableList(list) {
    'use strict’;
    this._version = 0;
    defineProperty(this, '_data', {
    enumerable: false,
    value: list || []
    });
    }
    'push pop shift unshift'.split(' ')
    .forEach(function (key) {
    'use strict';
    defineMethod(VersionableList.prototype, key, function () {
    this._data[key].apply(this._data, arguments);
    this._updateVersion();
    });
    });

    View Slide

  48. VersionableList
    function VersionableList(list) {
    'use strict’;
    this._version = 0;
    defineProperty(this, '_data', {
    enumerable: false,
    value: list || []
    });
    }
    'push pop shift unshift'.split(' ')
    .forEach(function (key) {
    'use strict';
    defineMethod(VersionableList.prototype, key, function () {
    this._data[key].apply(this._data, arguments);
    this._updateVersion();
    });
    });

    View Slide

  49. List Keys
    Object.keys(list);
    // ['_version']

    View Slide

  50. Usage Demo
    let list = new VersionableList([1, 2]);
    console.log(list.valueOf()); // [1, 2]
    console.log(list._version); // 0
    list.push(3);
    console.log(list.valueOf()); // [1, 2, 3]
    console.log(list._version); // 1

    View Slide

  51. 5 items
    Script Running Time
    Bindings Count

    View Slide

  52. 100 items
    Script Running Time
    Bindings Count

    View Slide

  53. Script Running Time
    Bindings Count
    10,000 items

    View Slide

  54. Lives @ GitHub
    (github.com/mgechev/versionable-collections)

    View Slide

  55. Application Specific
    Benchmarks

    View Slide

  56. What about Angular 2?

    View Slide

  57. In Angular 2
    • Unidirectional data flow
    • One-way data binding
    • No TTL (a single iteration of the $digest)
    • Less iterations over the watched expressions

    View Slide

  58. View Slide

  59. Angular 2 ❤ Immutability

    View Slide

  60. In Angular 2 we have
    components

    View Slide

  61. Lets think of the components as functions

    View Slide

  62. Component
    Input Output
    {  
       photoUrl:  “photo.png”,  
       firstName:  “Foo”,  
       lastName:  “Bar”,  
       age:  42

    }
    Perfect World
    Prof. Foo Bar
    42 years old

    View Slide

  63. Component
    Input Output
    Prof. Foo Bar
    42 years old
    Reality
    {  
       //  …

    }
    {  
       photoUrl:  “photo.png”,  
       firstName:  “Foo”,  
       lastName:  “Bar”,  
       age:  42

    }

    View Slide

  64. Component
    Input Output
    Reality
    Mutable state
    {  
       //  …

    }
    {  
       photoUrl:  “photo.png”,  
       firstName:  “Foo”,  
       lastName:  “Bar”,  
       age:  42

    }
    Prof. Foo Bar
    42 years old

    View Slide

  65. app
    todos
    todo todo
    user

    View Slide

  66. app
    todos
    todo todo
    user
    {  
       photoUrl:  “photo.png”,  
       firstName:  “Foo”,  
       lastName:  “Bar”,  
       age:  42,  
       updated:  1429260707251,  
       todos:  [{  
           title:  “Save  the  Universe”,  
           updated:  1429260907251,  
           completed:  true  
       },  {  
           title:  “Write  tests”,  
           updated:  1429260917251,  
           completed:  false  
       }]

    }

    View Slide

  67. app
    todos
    todo todo
    user
    {  
       photoUrl:  “photo.png”,  
       firstName:  “Foo”,  
       lastName:  “Bar”,  
       age:  42,  
       updated:  1429260707251,  
       todos:  [{  
           title:  “Save  the  Universe”,  
           updated:  1429260907251,  
           completed:  true  
       },  {  
           title:  “Write  tests”,  
           updated:  1429260917251,  
           completed:  false  
       }]

    }

    View Slide

  68. app
    todos
    todo todo
    user
    {  
       photoUrl:  “photo.png”,  
       firstName:  “Foo”,  
       lastName:  “Bar”,  
       age:  42,  
       updated:  1429260707251,  
       todos:  [{  
           title:  “Save  the  Universe”,  
           updated:  1429260907251,  
           completed:  true  
       },  {  
           title:  “Write  tests”,  
           updated:  1429260917251,  
           completed:  false  
       }]

    }

    View Slide

  69. app
    todos
    todo todo
    user
    user.todos
    user.todos[0] user.todos[1]
    user
    user

    View Slide

  70. app
    todos
    todo todo
    user
    Prof. Foo Bar
    42 years old
    1 todo pending
    [x] Save the Universe
    [x] Write tests

    View Slide

  71. app
    todos
    todo todo
    user
    user.todos =
    user.todos.filter(t => !t.completed });

    View Slide

  72. app
    todos
    todo todo
    user
    user.todos =
    user.todos.filter(t => !t.completed });

    View Slide

  73. app
    todos
    todo todo
    user
    user.todos =
    user.todos.filter(t => !t.completed });
    Change user.todos
    • todos will get wrong items
    • Angular needs to perform change
    detection over the whole tree

    View Slide

  74. app
    todos
    todo todo
    user
    user.todos =
    user.todos.filter(t => !t.completed });
    Change user.todos
    • todos will get wrong items
    • Angular needs to perform change
    detection over the whole tree

    View Slide

  75. app
    todos
    todo todo
    user
    user.todos =
    user.todos.filter(t => !t.completed });
    Change user.todos
    • todos will get wrong items
    • Angular needs to perform change
    detection over the whole tree

    View Slide

  76. Component
    Input Output
    Prof. Foo Bar
    42 years old
    Reality
    Mutable state
    {  
       //  …

    }
    {  
       photoUrl:  “photo.png”,  
       firstName:  “Foo”,  
       lastName:  “Bar”,  
       age:  42

    }

    View Slide

  77. Immutable Data & Object.freeze

    View Slide

  78. Why Object.freeze?

    View Slide

  79. immutable data structure
    mutable items

    View Slide

  80. View Slide

  81. View Slide

  82. View Slide

  83. View Slide

  84. View Slide

  85. View Slide

  86. View Slide

  87. Change Detection Reinvented
    Change Detection in Angular 2

    View Slide

  88. References
    • Boost the Performance of an AngularJS App Using Immutable Data - Part 1
    • Boost the Performance of an AngularJS App Using Immutable Data - Part 2
    • Even Faster AngularJS Data Structures
    • angular-immutable
    • Persistent Data Structures
    • Benchmarks
    • Versionable collections
    • Change Detection Reinvented
    • Change Detection in Angular 2
    • Immutable.js

    View Slide

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

    View Slide