angularjs_logo-svg

Другие статьи по AngularJS:

Бывают случаи, когда в приложении, написанном на Angular, встречаются изолированные блоки, в которых отображается большой набор данных, из-за которых приложение сильно тормозит. Например, это могут быть большие таблицы.

Существует не совсем изящный, но действенный (и иногда единственный) способ ускорения отрисовки интерфейса в таких ситуациях. Это переписывание подобных блоков с использованием ручного построения DOM.

Этот способ не всегда одинаково хорошо ускоряет отрисовку. Его эффективность зависит от особенностей шаблона, который отрисовывается с задержками. В этой статье описано, в каких случаях этот способ даёт наилучшие результаты, а также, какие у него есть альтернативы (например, использование шаблонизатора jQote2), и приведено сравнительное время отрисовки вёрстки разными способами на различных примерах.

Идеальная ситуация

На реальном проекте удалось ускорить отрисовку большой таблицы со статическими данными с 1060 мс до 120 мс. (Использовался Angular версии 1.5)

Шаблон выглядел следующим образом:

Пример 1

<div class="tbl-card clear-fix">
    <div class="tbl-card_left-part">
        <div class="tbl-card_left-part_head clear-fix">
            <div ng-repeat="item in ::vm.data.labels"
                 class="tbl-card_left-part_head_item" ng-bind="::item.Name"></div>
        </div>
        <div ng-repeat="row in ::vm.rows"
             class="tbl-card_left-part_row clear-fix">
            <div ng-repeat="cell in ::row" ng-bind="::cell"></div>
        </div>
    </div>
    <div class="tbl-card_right-part">
        <div class="tbl-card_right-part_title">Процент</div>
        <div class="tbl-card_right-part_scrollbox">
            <div class="tbl-card_right-part_head">
                <div ng-repeat="exam in ::vm.exams track by $index"
                     class="tbl-card_right-part_head_item" ng-bind="::exam"></div>
            </div>
            <div class="tbl-card_right-part_row" ng-repeat="row in ::vm.marks track by $index">
                <div class="tbl-card_right-part_cell" ng-repeat="cell in ::row track by $index">
                    <div ng-if="::cell !== null">
                        <span ng-bind="::cell.percent + '%'"></span>
                        <span class="tbl-card_right-part_cell--sub" ng-bind="::cell.total"></span>
                        <span class="fa" ng-if="::cell.up !== null"></span>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

Фрагмент кода на JS, написанного взамен Angular-шаблона:

angular
    .module('angular-test')
    .directive('jqTable', function () {
        return {
            restrict: 'E',
            scope: {
                items: '='
            },
            link: jqTable
        };
    });

...

function jqTable(scope, el, attr) {
    scope.$watch('items', function (value) {
        if (!value) {
            return;
        }
        renderItems(el, value);
    });
}

function renderItems(el, items) {

    ...

    var leftPartHtml = '<div class="tbl-card_left-part">';
    var labelsContainerHtml = '';
    _.each(data.labels, function (label) {
        labelsContainerHtml += '<div class="tbl-card_left-part_head_item">' + label.Name + '</div>';
    });
    labelsContainerHtml = '<div class="tbl-card_left-part_head clear-fix">' + labelsContainerHtml + '</div>';
    leftPartHtml += labelsContainerHtml;
    var rowsHtml = '';
    _.each(rows, function (row) {
        rowsHtml += '<div class="tbl-card_left-part_row clear-fix">';
        _.each(row, function (cell) {
            rowsHtml += '<div class="tbl-card_left-part_row_cell clear-fix' + (cell ? '' : ' tbl-card_left-part_row_cell--empty') + '">' + (cell || '') + '</div>';
        });
        rowsHtml += '</div>';
    });
    leftPartHtml += rowsHtml + '</div>';
    var rightPartHtml = '<div class="knowledges-card_right-part">';
    ...

    el[0].innerHTML = leftPartHtml + rightPartHtml;
}

Как видно, была создана директива, в которую передаются данные для построения таблицы. Построение разметки таблицы происходит в функции link директивы. Так сделано затем, чтобы не происходило изменений DOM из Angular-контроллера, т.к. это плохая практика.

В итоге Angular-шаблон, состоявший из 31 строки, был заменён на JavaScript-код длиной в 46 строк. Создание HTML-разметки в циклах выполняется с помощью ручной конкатенации строк, а не средствами jQuery, т.к. это даёт значительно лучший результат по времени.

Стоит отметить следующую особенность этого шаблона: в нём есть 4 независимых ngRepeat, которые перебирают разные наборы данных. Такая сложная, разветвлённая структура — признак, по которому можно предугадать, удастся ли значительно ускорить данный шаблон, переписав его на JavaScript. Далее показан пример более простого шаблона, для которого такого же значительного ускорения добиться невозможно.

Когда этот метод работает не так хорошо

Описанный способ ускорения работает менее эффективно для сравнительно простых шаблонов. Например:

Пример 2

<table>
    <tr ng-repeat="row in ::vm.items">
        <td ng-repeat="col in ::row.cols">
            <span ng-bind="::col.text"></span>
        </td>
    </tr>
</table>

Если в шаблон передаются данные из 500 строк и 20 столбцов, на его отрисовку уходит около 1230 мс (или 1680 мс, если на странице подключен jQuery — см. следующий раздел статьи).

После переписывания его на JavaScript на отрисовку уходит 700 мс.

Ситуация становится ещё сложнее, если данные в таблице не статические. Например, при удалении данных придётся удалять их вручную как из модели, так и из разметки. Пример:

<table>
    <tr ng-repeat="row in ::vm.items">
        <td ng-repeat="col in ::row.cols">
            <span ng-bind="::col.text"></span>
            <div class="delete-btn" ng-click="vm.removeCol(row, $index)"></div>
        </td>
        <td>
            <div class="delete-btn" ng-click="vm.removeRow($index)"></div>
        </td>
    </tr>
</table>

После переписывания на JS отрисовка шаблона на тестовых данных вместо 1940 мс (2400 мс с подключенным jQuery) занимает 1210 мс. И даже для простых операций вроде удаления элементов приходится писать много дополнительного кода и, возможно, подключать библиотеку jQuery:

var tableEl = $('<table/>', { html: tableHtml })
    .on('click', '.c-delete-row-btn', function() {
        var rowEl = $(this).parents('.c-row');
        items.splice(rowEl.attr('data-index'), 1);
        rowEl.remove();
    })
...

и т.д.

Стоит заметить, что в данном случае после каждого изменения данных внутри vm.items Angular не будет полностью перерисовывать директиву, так как выше (в функции jqTable) был использован $watch, который следит только за тем, на какой адрес памяти указывает переменная scope.items, но не за содержимым массива внутри этой переменной.

Ещё одно дополнение касательно jQuery

Любопытный факт: отрисовка Angular-шаблонов занимает разное время в зависимости от того, подключена ли на веб-странице библиотека jQuery. На конкретном примере отрисовка шаблона без наличия jQuery заняла 690 мс, а с jQuery — 1060 мс.

Причина этого в том, что для отрисовки DOM Angular обычно использует собственную оптимизированную версию jQuery, которая называется jqLite. В jqLite функции, используемые Angular, выполняются быстрее, чем в полной версии jQuery. В том случае, когда Angular при инициализации обнаруживает, что на странице уже загружена полная версия jQuery, он автоматически начинает использовать её вместо jqLite. Более подробно эта особенность описана в статье Mika Raento (ссылка ниже).

Сравнение разных методик

Далее приведены результаты сравнения скорости построения одной и той же разметки на 2-х описанных выше примерах с помощью Angular, jQuery, JavaScript и шаблонизатора jQote2. В ячейках таблицы указано время в миллисекундах.

Angular (with jQuery) Angular (without jQuery) jQuery jQote2 JavaScript
Пример 1 1060 690 400 160 120
Пример 2 1680 1230 1360 700 700

При отрисовке сложного шаблона (пример 1) лидирует JavaScript, jQote2 отстаёт совсем немного.

Интересно, что jQote2 справился с отрисовкой простого шаблона (пример 2) так же быстро, как JavaScript при ручном построении DOM.

jQuery показал значительно худшее время. В некоторых случаях Angular его опережает.

Из этих результатов можно сделать следующие выводы:

  1. Если нужно получить максимально возможное ускорение, лучше всего использовать чистый JavaScript.
  2. Если имеется необходимость использовать шаблонизацию, можно использовать, например, jQote2, либо другой шаблонизатор, который может сравниться с jQote2 по скорости отрисовки.

Другие статьи по AngularJS:

Полезные ссылки

  1. jsPerf: jquery vs createElement
  2. Mika Raento’s Tech Blog: Why is AngularJS slower with jQuery?