angularjs_logo-svg

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

AngularJS — один из популярных JS-фреймворков. В его основе — шаблонизация и 2-way data binding. В Angular-приложениях поддерживается модульность, из коробки работает клиентский роутинг.

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

Существует множество способов оптимизации приложений Angular. Здесь я попробую перечислить самые популярные их этих способов и сравнить их.

Почему возникают задержки в работе?

В Angular есть 2 серьёзных причины замедления работы приложений.

  1. Отрисовка большого количества элементов DOM. Это особенно заметно при использовании директивы ng-repeat.
  2. Увеличение количества $watchers

Первая причина проявляется в задержках при отрисовке страницы, вторая — в замедленном отклике при нажатиях мыши и вводе текста.

Проблемы первого рода возникают, например, если в шаблоне присутствуют ng-repeat, отрисовывающие множество элементов. Проблемы второго рода чаще возникают, если используется сложный data binding.

Немного информации о $watchers

Каждый раз, когда в шаблоне встречается единичный data binding (интерполяция, директивы ng-bind, ng-if, ng-show, ng-hide, ng-class и др.), Angular добавляет для него $watcher, который следит за значением привязанного выражения. И каждый раз, когда происходит некое действие — например, пользователь нажимает по кнопке с директивой ng-click, Angular проходит по всем созданным ранее $watchers, вычисляет новые значения выражений и сравнивает с прежними (именно так и работает волшебный 2-way binding). Затем он принимает решение о необходимости перерисовки элементов вёрстки. Это называется digest loop.

Причём, если data binding используется внутри директивы ng-repeat, то для каждой итерации создаётся отдельный набор $watchers. Пример:

<div ng-show="vm.isShown" ng-class="{active: vm.isActive}" ng-click="vm.showInfo()">{{vm.title}}</div>

Для такого шаблона будет создано 3 $watchers (ng-show, ng-class, {{vm.title}})

<div ng-repeat="d in vm.data" ng-show="d.isShown" ng-class="{active: d.isActive}" ng-click="vm.showInfo(d)">{{d.title}}</div>

Если массив vm.data содержит 100 элементов, то Angular создаст 301 $watcher (по 3 на каждую итерацию и один для ng-repeat).

Если $watchers становится слишком много, то интерфейс становится менее отзывчивым, т.к. в ответ на каждый щелчок мышью и каждое нажатие клавиши Angular приходится заново проверять значения всех $watchers.

Пример кода для отслеживания общего количества $watchers на странице, либо в отдельном компоненте

Также существует удобная утилита для анализа количества $watchers и времени отклика страницы: ng-stats.

Ускорение отрисовки DOM

Метод вычисления эффективности

Для проверки разных способов оптимизации я написал тестовый Angular-компонент такого вида:

<table>
	<tr ng-repeat="d in vm.data[vm.currentIndex]">
		<td>{{d.n}}</td>
		<td>{{d.title}}</td>
		<td>
			<div ng-repeat="c in d.content">
				<div ng-show="c.kind == 1" ng-click="vm.showInfo(1, c)">
					{{c.part1}}
				</div>
				<div ng-show="c.kind == 2" ng-click="vm.showInfo(2, c)">
					{{c.part2}}
				</div>
				<div ng-show="c.kind == 1" ng-click="vm.showInfo(1, c)">
					{{c.part1}}
				</div>
				<div ng-show="c.kind == 2" ng-click="vm.showInfo(2, c)">
					{{c.part2}}
				</div>
				<div ng-show="c.kind == 1" ng-click="vm.showInfo(1, c)">
					{{c.part1}}
				</div>
				<div ng-show="c.kind == 2" ng-click="vm.showInfo(2, c)">
					{{c.part2}}
				</div>
				<div ng-show="c.kind == 1" ng-click="vm.showInfo(1, c)">
					{{c.part1}}
				</div>
				<div ng-show="c.kind == 2" ng-click="vm.showInfo(2, c)">
					{{c.part2}}
				</div>
				<div ng-show="c.kind == 1" ng-click="vm.showInfo(1, c)">
					{{c.part1}}
				</div>
				<div ng-show="c.kind == 2" ng-click="vm.showInfo(2, c)">
					{{c.part2}}
				</div>
			</div>
		</td>
	</tr>
</table>

Среднее время отрисовки такого шаблона на тестовых данных составило 5800 мс.

Оптимизация ng-repeat

Значительная часть времени отрисовки страницы в Angular — это построение DOM. Таким образом, внутри директивы ng-repeat, к примеру, нужно минимизировать количество элементов DOM.

Использование ng-bind

Один из действенных способов ускорения отрисовки шаблонов — замена интерполяции ( {{expr}} ) на использование директивы ng-bind.

<td>{{d.n}}</td> => <td ng-bind="d.n"></td>

После применения такой замены по всему шаблону среднее время отрисовки — уже 5000 мс (-13%).

Этот метод работает только для простой интерполяции. Если в шаблоне заменить, например, такую конструкцию

{{c.part1 + ' ' + c.part2 + ' ' + c.part3 + ' ' + c.part4 + ' ' + c.part5}}

на такую:

<span ng-bind="c.part1"> <span ng-bind="c.part2"> <span ng-bind="c.part3"> <span ng-bind="c.part4"> <span ng-bind="c.part5">

отрисовка, наоборот замедлится, т.к. увеличилось количество элементов DOM.

Замена ng-show на ng-if

Во многих случаев ng-show/ng-hide в шаблонах можно заменить на ng-if. В результате Angular будет генерировать меньше элементов разметки. В нашем примере после замены ng-show на ng-if среднее время отрисовки уменьшилось до 4520 мс (-22%), а после объединения этого метода с предыдущим — до 4200 мс (-27%).

Этот метод также ускоряет отклик интерфейса, т.к. вместе с количеством элементов разметки уменьшается и количество $watchers.

Ускорение отклика интерфейса

One-time binding

Отличие этого метода оптимизации в том, что он срабатывает не за счёт уменьшения количества создаваемых элементов разметки, а за счёт уменьшения количество $watchers.

One-time binding появился в Angular версии 1.3. Для более ранних версий существуют дополнения вроде ng-bind-once.

Попробуем использовать one-time binding в тестовом шаблоне.

<a href="javascript:" ng-click="vm.update()">Update</a>
<table>
	<tr ng-repeat="d in vm.data[vm.currentIndex]">
		<td>{{::d.n}}</td>
		<td>{{::d.title}}</td>
		<td>
			<div ng-repeat="c in ::d.content">
				<div ng-show="::c.kind == 1" ng-click="vm.showInfo(1, c)">
					{{::c.part1}}
				</div>
				<div ng-show="::c.kind == 2" ng-click="vm.showInfo(2, c)">
					{{::c.part2}}
				</div>
				<div ng-show="::c.kind == 1" ng-click="vm.showInfo(1, c)">
					{{::c.part1}}
				</div>
				<div ng-show="::c.kind == 2" ng-click="vm.showInfo(2, c)">
					{{::c.part2}}
				</div>
				<div ng-show="::c.kind == 1" ng-click="vm.showInfo(1, c)">
					{{::c.part1}}
				</div>
				<div ng-show="::c.kind == 2" ng-click="vm.showInfo(2, c)">
					{{::c.part2}}
				</div>
				<div ng-show="::c.kind == 1" ng-click="vm.showInfo(1, c)">
					{{::c.part1}}
				</div>
				<div ng-show="::c.kind == 2" ng-click="vm.showInfo(2, c)">
					{{::c.part2}}
				</div>
				<div ng-show="::c.kind == 1" ng-click="vm.showInfo(1, c)">
					{{::c.part1}}
				</div>
				<div ng-show="::c.kind == 2" ng-click="vm.showInfo(2, c)">
					{{::c.part2}}
				</div>
			</div>
		</td>
	</tr>
</table>

Как видно, one-time binding использован во всех выражениях, кроме ng-repeat, т.к. мы предполагаем, что переменная vm.currentIndex будет изменяться, а содержимое элементов vm.data — нет. При каждом изменении vm.currentIndex всё содержимое блока ng-repeat будет перерисовано заново, поэтому two-way binding внутри него совершенно не нужен.

Среднее время отрисовки шаблона практически не изменилось.

Таким образом, этот метод оптимизации не спасает от проблем с отрисовкой DOM, но зато он поможет в том случае, если проблемы с замедлением работы приложения вызваны большим количеством $watchers.

Event delegation

Ещё одна из причин замедления Angular-приложений — множество обработчиков событий. Например:

<div class="container">
	<div ng-repeat="item in vm.items">
		<div ng-click="vm.doSomething($index)"></div>
	</div>
</div>

Для каждого элемента из массива vm.items создаётся div, и для каждого из них происходит подписывание на событие click. В некоторых случаях это замедляет работу браузера.

Суть метода event delegation состоит в том, что подписывание на событие click можно производить только 1 раз — для родительского элемента. (в нашем случае — .container). В Angular для этого можно написать директиву наподобие такой:

angular.module('other', []).directive('clickChildren', function ($parse) {
	return {
		restrict: 'A',
		link: function (scope, el, attr) {
			el.on('click', attr.selector, function(e) {
				var fn = scope.$eval(attr.clickChildren);
				var idx = e.target.getAttribute('data-index');
				fn(idx);
			});
		}
	};
});

(существуют разные варианты — к сожалению, универсального нет, т.к. в разных проектах делегацию событий приходится реализовывать по-разному)

И доработать шаблон:

<div class="container" click-children="vm.doSomething" selector=".clickable">
	<div ng-repeat="item in vm.items">
		<div class="clickable" data-index="{{$index}}"></div>
	</div>
</div>

Минус этого метода в том, что он может замедлить отрисовку DOM. При использовании делегирования событий на предыдущем тестовом примере, время отрисовки шаблона увеличилось до 6400 мс (+10%), т.к. в шаблоне внутри ng-repeat добавились атрибуты с data-binding.

Итоги

В этой статье были перечислено несколько из распространённых методов оптимизации Angular-приложений. Как видно, все они в разных ситуациях работают по-разному, и в некоторых случаях некоторые из этих методов не столько ускоряют, сколько замедляют работу приложения — поэтому надо применять их разборчиво.

И если необходимо подобрать JS-фреймворк для будущего проекта, в котором ожидается нагруженный UI, надо допускать вероятность, что выбор Angular окажется не лучшим решением.

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

Ссылки

  1. Optimizing AngularJS: 1200ms to 35ms — Scalyr Blog
  2. Using Track-By With ngRepeat In AngularJS 1.2
  3. Using $scope.$digest() As A Performance Optimization In AngularJS
  4. Using ngRepeat With ngInclude Hurts Performance In AngularJS