AngularJS ng-repeat is slow

Viewed 451

It is not like it is slow on rendering many entries. The problem is that whenever the $scope.data got updated, it adds the new item first at the end of the element, then reduce it as it match the new $scope.data.

For example:

<div class="list" ng-repeat="entry in data">
    <h3>{{entry.title}}</h3>
</div> 

This script is updating the $scope.data:

$scope.load = function() {
    $scope.data = getDataFromDB();
}

Lets say I have 5 entries inside $scope.data. The entries are:

[
    {
        id: 1,
        title: 1
    },
    {
        id: 2,
        title: 2
    },
    ......
]

When the $scope.data already has those entries then got reloaded ($scope.data = getDataFromDB(); being called), the DOM element for about 0.1s - 0.2s has 10 elements (duplicate elements), then after 0.1s - 0.2s it is reduced to 5.

So the problem is that there is delay about 0.1s - 0.2s when updating the ng-repeat DOM. This looks really bad when I implement live search. Whenever it updates from the database, the ng-repeat DOM element got added up every time for a brief millisecond.

How can I make the rendering instant?


EDITED

I will paste all my code here:

The controller:

$scope.search = function (table) {
    $scope.currentPage = 1;
    $scope.endOfPage = false;
    $scope.viewModels = [];
    $scope.loadViewModels($scope.orderBy, table);
}

$scope.loadViewModels = function (orderBy, table, cb) {
    if (!$scope.endOfPage) {
        let searchKey = $scope.page.searchString;
        let skip = ($scope.currentPage - 1) * $scope.itemsPerPage;
        let searchClause = '';

        if (searchKey && searchKey.length > 0) {
            let searchArr = [];
            $($scope.vmKeys).each((i, key) => {
                searchArr.push(key + ` LIKE '%` + searchKey + `%'`);
            });
            searchClause = `WHERE ` + searchArr.join(' OR ');
        }

        let sc = `SELECT * FROM ` + table + ` ` + searchClause + ` ` + orderBy +
            ` LIMIT ` + skip + `, ` + $scope.itemsPerPage;
        sqlite.query(sc, rows => {
            $scope.$apply(function () {
                var data = [];
                let loadedCount = 0;
                if (rows != null) {
                    $scope.currentPage += 1;
                    loadedCount = rows.length;
                    if (rows.length < $scope.itemsPerPage)
                        $scope.endOfPage = true

                    for (var i = 0; i < rows.length; i++) {
                        let item = rows.item(i);
                        let returnObject = {};
                        $($scope.vmKeys).each((i, key) => {
                            returnObject[key] = item[key];
                        });
                        data.push(returnObject);
                    }
                    $scope.viewModels = $scope.viewModels.concat(data);
                } 
                else
                    $scope.endOfPage = true;

                if (cb)
                    cb(loadedCount);
            })
        });
    }
}

The view:

<div id="pageContent" class="root-page" ng-controller="noteController" ng-cloak>
    <div class="row note-list" ng-if="showList">
        <h3>Notes</h3>
        <input ng-model="page.searchString" id="search"
               ng-keyup="search('notes')" type="text" class="form-control"
               placeholder="Search Notes" style="margin-bottom:10px">
        <div class="col-12 note-list-item"
             ng-repeat="data in viewModels track by data.id"
             ng-click="edit(data.id)"
             ontouchstart="touchStart()" ontouchend="touchEnd()"
             ontouchmove="touchMove()">
            <p ng-class="deleteMode ? 'note-list-title w-80' : 'note-list-title'"
               ng-bind-html="data.title"></p>
            <p ng-class="deleteMode ? 'note-list-date w-80' : 'note-list-date'">{{data.dateCreated | displayDate}}</p>
            <div ng-if="deleteMode" class="note-list-delete ease-in" ng-click="delete($event, data.id)">
                <span class="btn fa fa-trash"></span>
            </div>
        </div>
        <div ng-if="!deleteMode" ng-click="new()" class="add-btn btn btn-primary ease-in">
            <span class="fa fa-plus"></span>
        </div>
    </div>
    <div ng-if="!showList" class="ease-in">
        <div>
            <div ng-click="back()" class="btn btn-primary"><span class="fa fa-arrow-left"></span></div>
            <div ng-disabled="!isDataChanged" ng-click="save()" class="btn btn-primary" style="float:right">
                <span class="fa fa-check"></span>
            </div>
        </div>
        <div contenteditable="true" class="note-title"
             ng-bind-html="selected.title" id="title">
        </div>
        <div contenteditable="true" class="note-container" ng-bind-html="selected.note" id="note"></div>
    </div>
</div>

<script src="../js/pages/note.js"></script>

Calling it from:

$scope.loadViewModels($scope.orderBy, 'notes');

The sqlite query:

query: function (query, cb) {
    db.transaction(function (tx) {
        tx.executeSql(query, [], function (tx, res) {
            return cb(res.rows, null);
        });
    }, function (error) {
        return cb(null, error.message);
    }, function () {
        //console.log('query ok');
    });
},

It is apache cordova framework, so it uses webview in Android emulator.


My Code Structure

<html ng-app="app" ng-controller="pageController">
    <head>....</head>
    <body>
        ....
        <div id="pageContent" class="root-page" ng-controller="noteController" ng-cloak>
            ....
        </div>
    </body>
</html>

So there is controller inside controller. The parent is pageController and the child is noteController. Is a structure like this slowing the ng-repeat directives?

Btw using track by is not helping. There is still delay when rendering it. Also I can modify the entries as well, so when an entry was updated, it should be updated in the list as well.


NOTE

After thorough investigation there is something weird. Usually ng-repeat item has hash key in it. In my case ng-repeat items do not have it. Is it the cause of the problem?

3 Answers

One approach to improve performance is to use the track by clause in the ng-repeat expression:

<div class="list" ng-repeat="entry in data track by entry.id">
    <h3>{{entry.title}}</h3>
</div> 

From the Docs:

Best Practice: If you are working with objects that have a unique identifier property, you should track by this identifier instead of the object instance, e.g. item in items track by item.id. Should you reload your data later, ngRepeat will not have to rebuild the DOM elements for items it has already rendered, even if the JavaScript objects in the collection have been substituted for new ones. For large collections, this significantly improves rendering performance.

For more information, see

In your html, try this:

<div class="list" ng-repeat="entry in data">
    <h3 ng-bind="entry.title"></h3>
</div> 

After thorough research, I found my problem. Every time I reset / reload my $scope.viewModels I always assign it to null / empty array first. This what causes the render delay.

Example:

$scope.search = function (table) {
    $scope.currentPage = 1;
    $scope.endOfPage = false;
    $scope.viewModels = []; <------ THIS
    $scope.loadViewModels($scope.orderBy, table);
}

So instead of assigning it to null / empty array, I just replace it with the new loaded data, and the flickering is gone.

Related