3

I understand that two way data binding can be expensive and slow. For example, imagine a toy grocery list app that lets you list grocery items and their prices, and shows you the sum of the prices (CodePen).

HTML

<div ng-app="app">
  <div ng-controller="MainCtrl">
     <table>
       <thead>
         <tr>
           <th>Name</th>
           <th>Price</th>
         </tr>
       </thead>
       <tbody>
         <tr ng-repeat="item in items">
           <td>{{ item.name }}</td>
           <td>
             <input type="number" ng-model="item.price">
           </td>
         </tr>
       </tbody>
    </table>
    <p>Total: {{ getTotal() }}</p>
  </div>
</div>

JS

angular
  .module('app', [])
  .controller('MainCtrl', function ($scope) {
    $scope.items = [{
      name: 'Milk',
      price: 3.00,
    }, {
      name: 'Eggs',
      price: 2.50
    }, {
      name: 'Bread',
      price: 2.00
    }];
    $scope.getTotal = function () {
      let total = 0;

      $scope.items.forEach(function (item) {
        total += item.price
      });

      return total;
    };
  })
;

Imagine that the user types into the milk price input field. In a perfect world, Angular would know to make sure the milk input field reflects the new value, and to update the total. But in reality, Angular doesn't know that those are the only things to update. What if I had a "milk + bread cost" field? Then that field would need to be updated too.

So Angular goes through all of the model properties that are being displayed in the view, and asks "Have you changed? If so, I will rerender you." If this list is long, it would obviously take a long time.

But in something like React or Vue, my impression is that it should take a similarly long time. Here is an implementation of the same toy app in Vue (CodePen):

HTML

<div id="app">
  <table>
    <thead>
      <tr>
        <th>Name</th>
        <th>Price</th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="item in items">
        <td>{{ item.name }}</td>
        <td>
          <input 
            type="number" 
            v-bind:value="item.price" 
            v-on:change="setItemPrice(item, $event)"
          >
        </td>
      </tr>
    </tbody>
  </table>
  <p>Total: {{ total }}</p>
</div>

JS

new Vue({
  el: '#app',
  computed: {
    total: function () {
      let total = 0;

      this.items.forEach(function (item) {
        total += item.price;
      });

      return total;
    },
  },
  data: {
    items: [{
      name: 'Milk',
      price: 3.00,
    }, {
      name: 'Eggs',
      price: 2.50
    }, {
      name: 'Bread',
      price: 2.00
    }],
  },
  methods: {
    setItemPrice: function (item, event) {
      item.price = Number(event.target.value);
    },
  },
});

My understanding is that after the change event fires and the handler method (setItemPrice) finishes running, Vue will compute a new virtual DOM, diff it with the previous virtual DOM, figure out the minimal set of DOM mutations to perform, and then perform them.

I would think that the process of traversing the DOM and seeing if there are any differences would take at least as long as the process of traversing the watchers array during dirty checking. My impression is that it would take longer. Just look at the HTML:

<table>
  <thead>...</thead>
  <tbody>
    <tr>
      <td>Milk</td>
      <td>3</td>
    </tr>
    <tr>
      <td>Bread</td>
      <td>2.5</td>
    </tr>
    <tr>
      <td>Eggs</td>
      <td>2</td>
    </tr>
  </tbody>
</table>
<p>Total: 7.5</p>

If n = the size of the grocery list, dirty checking has to iterate through n items in the watchers array. But in traversing the DOM, you have to iterate through the n <tr> tags plus everything else.

What am I missing about why one way data flow is (seen as) significantly more performant than two way data binding with dirty checking?

  • I can't comment on these implementations, but there's definitely faster ways to determine what changed in a collection, than iterating over every item. One typical way is to receive a notification when changes occur, and update a "dirty list." – Frank Hileman Jun 8 '18 at 22:26
  • @FrankHileman It seems that the author of Angular decided against change listeners because they lead to bad syntax and annoying corner cases stackoverflow.com/questions/9682092/… – Adam Zerner Jun 9 '18 at 0:16
  • Right, it requires careful thought and optimization, and you can't use it for plain old objects. Typically there is a corresponding dirty flag to avoid some of the problems they describe. – Frank Hileman Jun 12 '18 at 19:04
3

I guess you're trying to compare different things, but I see what you mean. I would rephrased your question to: Why is dirty checking in AngularJS slower than building DOM diffs.

By the way, Angular2+ also dirty checks values used in template expressions. Say you click a button, angular then runs change detection cycle, and it goes from the top component to the very bottom, and what it is doing it is checking all the model values used in expressions by reference with their previous values. So it dirty checks the values. The difference with AngularJS is that it does not check it twice. And also it does not make deep comparison (which is also disabled in AngularJS by default, but could be turned on as a third argument of $watch function).

But comparing dirty checking and building DOM diffs is a bit incorrect, cause they are different things, and you can actually combine them.

What is the difference then. With dirty checking you need to check everything on every change or event. It could be very performant and Angular2+ proves it. But, React on the other hand knows exactly which component has been changed cause you trigger setState(). So instead of checking everything in your app, it will just try to build the diff of the current component tree.

So in conclusion, dirty checking is not bad, it depends on the implementation, and on your app. You might find that in your application it is faster to dirty check values than it is to build the diffs, or vise versa. Here is an interesting performance comparison of React/View/Angular5

  • Huh. That makes a lot of sense that Angular 2 only going through stuff once is a lot faster than Angular 1 running multiple digest cycles. And it also makes a lot of sense that setState() only builds the diff off of the current component tree. 1) I don't see why Angular can't/didn't do this. Why it ran $scope.$apply to dirty check the whole app rather than $scope.$digest and only check the current scope and it's children. 2) So it seems that dirty checking isn't inherently slower than virtual DOM stuff. Am I understanding you correctly there? – Adam Zerner Jun 10 '18 at 5:33
  • 1
    @AdamZerner 1) using $scope.$digest() by default is risky, say you've removed all the users by calling UserService.removeAll(), if somewhere in your app you have a watcher like $watch(() => userService.users.length, () => {// do smth.}) it won't be triggered if it is located in another scope few levels above. This is why by default AngularJS checks everything. 2) That's right. Inherently it doesn't, reference check is actually super fast. – Pavel Jun 10 '18 at 12:24

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.