2
\$\begingroup\$

I have a big problem with performance with a web application using AngularJS. I currently have a grid that renders an array of objects. It looks something like this.

<table data-md-table data-md-row-select="!hideCheckboxes" data-ng-model="row.selected"
               md-progress="promise" class="border-r">
  <thead ng-if="!hideHead" md-head data-md-order="" md-on-reorder="onReorder">
  <tr md-row>
    <th md-column md:order:by="mark" ><span>Title 1</span></th>
    <th md-column data-md-order-by="desc" class="padder"><span>Title 2</span></th>
    <th md-column data-md-order-by="period" class="padder" ><span>Title 3</span></th>
    <th md-column data-md-order-by="instance" class="padder"><span>Title 4</span></th>
    <th md-column data-md-order-by="endDate" class="padder"><span>Title 5</span></th>
    <th md-column data-md-order-by="itemState" class="padder"><span>Title 6</span></th>
  </tr>
  </thead>
  <!--Data table-->
  <tbody md-body>
  <tr md-row md:select="row" data-md-on-select="log" md-on-deselect="deselect"
      data-ng-repeat="row in myArray.data">
    <td md-cell>
      {{row.mark}}
    </td>
    <td md-cell>
      <b>{{row.desc}}</b>
    </td>
    <td md-cell>
      {{row.period | date:'yyyy MMMM' : 'UTC'}}
    </td>
    <td md-cell class="txt-cnt">
      {{row.instance}}
    </td>
    <td md-cell ng-if="row.inter === null">
      <md-icon md-svg-src="icons/ic_keyboard_arrow_down_green_24px.svg"></md-icon>
      {{row.fecCorrida | date:'dd/MM/yyyy'}}
      <br/>
      <md-icon md-svg-src="icons/ic_keyboard_arrow_down_red_24px.svg"
               ng-if="row.endDate != null"></md-icon>
      {{row.endDate | date:'dd/MM/yyyy'}}
    </td>
    <td md-cell ng-if="row.inter !== null">
      <md-icon md-svg-src="icons/ic_keyboard_arrow_down_green_24px.svg"></md-icon>
      {{row.fecCorrida | date:'dd/MM/yyyy HH:mm:ss'}}
      <br/>
      <md-icon md-svg-src="icons/ic_keyboard_arrow_down_red_24px.svg"
               ng-if="row.endDate != null"></md-icon>
      {{row.endDate | date:'dd/MM/yyyy HH:mm:ss'}}
    </td>

    <td md-cell style="width:100px">
      <div ng-class="row.itemState"><b>{{row.itemState}}</b></div>
    </td>

    <td md-cell style="padding:0 23px 0 0">
      <md-icon ng-if="row.itemState == 'En Curso'"
               ng-click="updaterow(row)"
               ng-class="{ 'running-update-agenda': row.updating }"
               md-svg-src="icons/ic_refresh_24px.svg">
        <md-tooltip md-direction="left">Update</md-tooltip>
      </md-icon>
    </td>
    <td md-cell style="padding:0 23px 0 0">
      <md-icon ng-click="toggleRightNavAgenda(row)" md-svg-src="icons/ic_list_grey_24px.svg"
               ng-hide="row.itemState === 'PENDING'">
        <md-tooltip md-direction="left">Detail</md-tooltip>
      </md-icon>
    </td>
    <td md-cell style="padding:0 23px 0 0">
      <md-tooltip md-direction="left">ON / OFF</md-tooltip>
      <md-switch ng-model="row.off" aria-label="ON / OFF"
                 ng-change="on(row)" class="md-success md-body-1"
                 ng-class="{'md-checked': row.off}"
                 ng-value="row.off" ng-false-value="false" ng-true-value="true" style="margin: 0 !important;">
      </md-switch>
    </td>
    <td md-cell style="padding:0 23px 0 0">
      <md-icon class="{{row.itemState}}-boton play"
               ng-hide="row.itemState === 'OK' || row.itemState === 'Working'"
               ng-click="row.running || runProcess($index)"
               md-svg-src="icons/ic_play_arrow_grey_24px.svg">
        <md-tooltip md-direction="left">
          Run
        </md-tooltip>
      </md-icon>
    </td>
    <td md-cell style="padding:0 23px 0 0">
      <md-icon ng-if="row.itemState == 'En Curso'"
               ng-click="correct(row)"
               ng-class="{ 'running-update': row.updating }"
               md-svg-src="icons/ic_build_24px.svg">
        <md-tooltip md-direction="left">
          Correct
        </md-tooltip>
      </md-icon>
    </td>

  </tr>
  </tbody>
</table>

Some of these bindings won't change the rows, but some will. The cell that have md-icons are buttons that cause the row's state to change, and render a different image in one of the other cells to let the user know the row is being processed.

I ran this function in the console and found that this page is keeping 15000 watches (!) overall, most of them (around 95%) within this grid alone. I've been reading about watches and $digest in Angular, but apparently the only solutions are making the rows static or using some sort of pagination (which that client can't be convinced to use).

Is there anything else you'd recommend?

\$\endgroup\$
2
\$\begingroup\$

It's hard to imagine what exactly this table is doing and how it is looking when rendered just by looking at this HTML code. But it seems that your case is a good example of the performance boundaries of AngularJS.

Anyway I will try to give you some ideas on how you could possibly improve the performance to an acceptable state:

Tell Angular to use one-way binding where ever possible

Currently Angular watches for changes at any of your row's properties. If one row displays (or evaluates) 25 properties and your array consists of 600 objects, you have 600 * 25 + 1 = 15001 watches. So, this happens very fast. Try to use one-way binding by prefixing evaluated properties with :: where possible. Assuming that row.mark is static within one row, do this:

<td md-cell>
  {{ ::row.mark }}
</td>

Use ng-repeat with track by

A quick fix is using the track by statement of ng-repeat. Simply tell Angular how to index your array. If you have a property in your object which could be used as an index, you could take this one. If not, simply use $index, which is Angular's internal array index. In your case it looks like this:

...
<tr md-row md:select="row" data-md-on-select="log" md-on-deselect="deselect" 
data-ng-repeat="row in myArray.data track by $index">
...

Do not use filters within ng-repeat

Angular's filters are a very convenient feature. I like it a lot. Nonetheless it comes with a big performance trade-off. Every digest-cycle Angular will iterate through your array and evaluate the filters you set. You are using the date filter quite often. From my point of view you could precalculate the text representation of your dates once, at the initialization of your array (after fetching it from the backend?). You can use any Angular filter also in your code, the same way you are doing it in your template. Imagine, the following is the service which fetches your table's data:

function TableService($filter) {

   var dateFilter = $filter('date');

   var tableData;

   fetchDataFromServerAsync()
       .then(function(data) {
           tableData = data.map(function(row) {
              row.period = dateFilter(row, 'yyyy MMMM', 'UTC');
              // TODO: apply filter to other properties
           });
       });
}

If you need to work with the date instances in your Angular app, you could convert the dates like above and save them in a new property. Of course, then you need to convert the text representation whenever your dates change. It's another trade-off.

Order your Array with plain JavaScript methods

It looks like the library you are using, Material Design Data Table, is using the orderBy feature of ng-repeat under the hood. This is great, as it's the Angular way to sort arrays easily. But this also comes with a big performance trade-off. Sorting with orderBy is not a thing you should do with large arrays. Instead you could implement sorting in your controller's code (or your table service). For this you would need to give up your frameworks built-in features. Instead of using md-order-by you would write a vanilla JavaScript sort method:

function YourTableController($scope) {
    $scope.myArray = [ ... ];

    $scope.sortByPeriod = function() {
        $scope.myArray.sort(function(a, b) {
            // I don't know if the following makes sense in your case
            // it's just an example
            return new Date(b.period) - new Date(a.period);
        });
        // I think you need to call $scope.$apply() here in order to
        // update your view. You have to try it out.
    };
}

Then in your template, attach ng-click="sortByPeriod()" where you want to trigger the sorting.

Use lazy loading

Your client doesn't want pagination, okay. But did you ask him, whether lazy loading is okay? If you already put sorting into your own JavaScript implementation, you could sort your array, then cut a piece out (e.g. 100 instead of 600 items) and put it into the rendered table. Then, if the user scrolls down, either the lazy loading is triggered automatically or by a button click. If this happens, you take the next slice of 100 items from your array. I guess you got the point...

Check if there is a performance bottleneck in Material Design Data Table

If the above is not enough, you could dig deeper and find out, if Material Design Data Table's implementation is performant enough for your case. I know Angular Material and I know that it isn't fast under any circumstances. Maybe the same applies to this library.

Please note that I didn't test my ideas for correctness and/or side-effects. So you may run into problems when combining them.

Sources:

AngularJS – ngRepeat Performance Watchers

AngularJS API Reference - date filter

\$\endgroup\$

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.