Web Development – Why One-Way Data Flow is Faster Than Two-Way Data Binding

Architecturefront-endperformanceweb-development

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?

Best Answer

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

Related Topic