Чем измерять?
Измерять производительность приложения удобно расширением Batarang для браузера Chrome. Этот инструмент показывает время выполнения каждого выражения:
Измерять количество самих наблюдателей (watchers) удобно расширением Angular watchers.
К чему стремиться?
Восприятие приложения во много зависит от времени выполнения цикла $digest. Misko Hevery в своем знаменитом ответе на stackoverflow говорит, что любые изменения быстрее 50 ms незаметны для человека и следовательно их можно рассматривать как «мгновенные». Соответственно, чтобы пользователи не чувствовали «притормаживаний», мы должны уложиться в 50–100 ms на каждом $digest.
Как этого добиться?
В каждом цикле $digest вызываются все watcher-функции и проверяется вся scope-модель на наличие изменений. Условно говоря, время выполнения цикла $digest = количество наблюдателей * время их выполнения. Таким образом, чтобы оптимизировать работу $digest нужно или уменьшить количество наблюдателей или увеличить скорость их вычисления.
$scope.$watch
$scope.myVar = function() {
return 2 * 2;
}
$scope.$watch(myVar, function() {
alert('myVar has changed!');
});
Проверка изменений $scope.myVar будет происходить минимум один-два раза при каждом цикле $digest. Поэтому, старайтесь избегать дорогостоящих вычислений под наблюдением.
ng-show и ng-switch
Если вы скрываете блок с помощью ng-hide или ng-show, внутренние элементы не удаляются из DOM, а просто скрываются через CSS-стиль display: none. Поэтому все {{expression}} внутри этих элементов будут вычисляться при каждом проходе $digest.
Не наблюдайте за переменными в невидимых элементах, или используйте ng-switch, чтобы удалить скрытые элементы из самого DOM.
{{myName|filter}}
Каждый фильтр в AngularJS выполняется минимум один-два раза при каждом цикле $digest. Старайтесь не использовать тяжеловесные вычисления в логике фильтров.
$http
При каждом обращении к $http (и получении ответа) вызывается цикл $digest. Сократите количество обращений к серверу, модифицировав передаваемые данные.
ng-bind
<p>Lorem ipsum dolor sit amet ... mollit anim id {{est}} laborum.</p>
В данном примере, Angular будет наблюдать не только за {{est}}, но и за всем текстом внутри <p>. Таким образом, весь текст (а он может быть действительно большим!) будет хранится в памяти. Чтобы избежать подобных ситуаций, используйте привязку ng-bind:
<p>Lorem ipsum dolor sit amet ... mollit anim id <span ng-bing="est">est</span> laborum.</p>
ng-repeat
Директива ng-repeat является одной из самых затратных с точки зрения производительности. Каким-либо простых способов ее оптимизации не существует. Поэтому, избегайте ng-repeat при работе с большим массивом данных. Урезайте набор данных до того, как они попадут в ng-repeat.
{{::value}}
Начиная с версии 1.3 в AngularJS появилась такая интересная особенность как одноразовое связывание (one-time binding). Обычно значения передаются в DOM таким образом:
<h1>{{title}}</h1>
Чтобы использовать одноразовое связывание, нужно добавить :: перед значением:
<h1>{{::title}}</h1>
При одностороннем связывании, $watch удалится после вывода первых данных, а значит мы выиграем в производительности. С другой стороны, любые обновления модели не повлияют на представление.
UI
В конечно итоге, самым простым способом оптимизации Angular, является уменьшение количества наблюдателей. Механизм двухстороннего связывания насколько удобен, что очень часто мы используем его там, где это неоправданно. Всем известно о теоретическом пределе «2000 наблюдателей», но если бы мы задумались об удобстве пользователей, то даже близко не подобрались бы к этому пределу.
Действительно ли наши пользователи могут осмыслить одновременное обновление двух тысяч переменных? Может лучше модифицировать приложение таким образом, чтобы внимание пользователя было сосредоточено только на важных в данный момент элементах управления? Это не только улучшит пользовательский опыт, но и сократит количество циклов $digest, выполняемых в единицу времени.
Ссылки по теме:
Ускоряем angular.js
Speeding up AngularJS
Angular: Performance
Комментарии (31)
lega
30.10.2015 12:55Условно говоря, время выполнения цикла $digest = количество наблюдателей * время их выполнения.
Можно умножить на кол-во циклов, что-б получить время выполнения $digest, т.к. при плохом использовании $watch, можно увеличить кол-во циклов в каждом $digets.
используйте ng-switch, чтобы удалить скрытые элементы из самого DOM.
Так же можно использовать ng-if
Каждый фильтр в AngularJS выполняется минимум один-два раза при каждом цикле $digest
Не один-два, а ровно один раз в каждом цикле, и то если это stateful фильтр. А вызов (обычного) «stateless» фильтра будет зависеть — изменилось ли входящее значение.
Сократите количество обращений к серверу, модифицировав передаваемые данные.
Так же можно использовать useApplyAsync
Так же не сказано про самую главную «оптимизацию» — компонентный подход (сейчас это как рекомендация для многих фреймворков*), таким образом можно, например, 100к ватчей разбить* на 100 компонентов по ~1000 ватчей в каждом, таким образом $digest цикл будет обрабатывать не 100К а всего лишь 1000 ватчей, а это быстрее чем 1мс (для простых ватчей).NikitaKA
30.10.2015 14:33+2Так же не сказано про самую главную «оптимизацию» — компонентный подход
Не могли бы вы привести пример?lega
30.10.2015 15:59Например если взять комментарии с хабра, каждый комментарий имеет имя автора, аватарку, время, контент, голоса и кнопки изменения рейтинга, на стороне редактирования несколько контролов с таймером. Главное что каждый комментарий более-менее независим от окружающих. Конечно большинство биндингов могут быть bindOnce или oneTimeBinding, но если на вскидку взять 30 ватчей на весь функционал одного комментария, то на странице с 300 комментариями будет 9000 ватчей — не так и мало, особенно если $digest будет вызываться на каждое нажатие кнопки (при написании комментария, например).
Дак вот если сделать каждый комментарий как «независимый» компонент (директива), со своим (root) scope, то при работе с одним комментарием, будет дергаться $digest только на 30 ватчей — и это будет работать быстро, несмотря на кол-во комментарий на странице.
Так же Angular 2 «вводит» компоненты (хотя я не знаю на сколько они изолированы), для Angular 1 разработчики создают rootScope под каждый компонент (возможно в виде директивы).
Вот 2 одинаковых примера на Angular Light — список блоков с контролами, только в одном используется scope: 'root' — т.е. каждый блок имеет свой (root) scope: пример с общим scope, пример где каждый блок имеет отдельный scope, если покликать на кнопки то видно что в первом примере любые действия влияют на все приложение — добавляете watch в один блок, тем самым он нагружает все остальные блоки, когда во втором примере этого не происходит.
Так же такие элементы как верхнее меню, выскакивающие диалоги, блоки с рекламой, «Популярное за сутки» и пр. никак не связаны с комментариями и главным контентом страницы, все их имеет смысл делать самостоятельными компонентами, а не «кусочком» монолитного приложения. Это не только «ускорят» приложение но и хорошо для архитектуры приложения, имхо.sAntee
30.10.2015 16:47я не знаю, что там происходит с Angular Light, но в обычном Angular'е, никаких дополнительных $rootScope на каждую директиву не создается. $rootScope это вообще сервис, который по определению синглтон в ангуляре. Isolated scope это другое. В производительности можно выиграть, если вызывать $digest на этом скоупе, вместо $apply ($rootScope.$digest). Но это может быть не всегда возможно.
lega
30.10.2015 17:27+1Angular'е, никаких дополнительных $rootScope на каждую директиву не создается
Штатно не создается, но можно сделать новый rootScope вручную (не через сервис), и у него будет свой $digest цикл. Правильно это или нет, каждый принимает решение за себя, но некоторые это используют.sAntee
31.10.2015 11:22А можно пример как это сделать?
lega
31.10.2015 11:47Например так:
newRootScope = new $rootScope.constructor()
Хотя я видел более красивый вариант.sAntee
31.10.2015 17:32А что это дает в сравнении со $scope.$new() с вызовом $digest на нем? В чайлдовых директивах и темплейтах все равно будет использоваться то что возвращает $injector сервис. Вся целостность приложения ломается. Либо я чего-то не понимаю.
lega
31.10.2015 23:12Вот вариант со своими инжектами.
sAntee
31.10.2015 23:41да вот только это три отдельных приложения.
lega
01.11.2015 11:37Главное работает, дальше можно сделать красивые обертки и использовать.
А вообще, апи у Angular 1 в этом плане не удачное, и то что в Angular 2 делают по другому — это как подтверждение.sAntee
01.11.2015 12:11Черт, естественно оно работает, чего б ему не работать :) Вот только
Вся целостность приложения ломается
Как вы в таком случае намереваетесь налаживать какое-то взаимодействие между этими компонентами? И, главное, зачем так извращаться? Если приходиться это делать чтобы сократить количество watcher'ов, то тут проще вообще от data-binding'а в шаблонах избавиться, чем по инстансу приложения создавать на контрол. Про удачность апи это да.
sAntee
01.11.2015 12:59В общем ок, предлагаю согласиться на том, что все ждем 2-й ангуляр :)
lega
01.11.2015 22:44Как вы в таком случае намереваетесь налаживать какое-то взаимодействие между этими компонентами?
Я не намереваюсь, я использую Angular Light и у меня таких проблем нет.
Про удачность апи это да. ждем 2-й ангуляр :)
Ангуляр 2 ещё не зарелизился, а в нем уже не всехорошокак хотелось бы, хотя фанатам буде плевать. Так же кроме Ангуляра есть полно других фреймворков (и не плохих тоже).sAntee
01.11.2015 23:23Я не намереваюсь, я использую Angular Light и у меня таких проблем нет.
Ну так спорили же за обычный. Идеальных фреймворков вообще не бывает, посмотрим чего они там намудрят…
NikitaKA
30.10.2015 17:02Спасибо за ценный комментарий.
Подскажите, пожалуйста, по аргументам в ваших примерах:scope:'root'/'isolate'
— в документации написано, что аргументы могут бытьtrue/'isolate'
, но в ваших примерах используется'root'
, который ведет себя как-то по-особенному, а все остальное (true, 'isolate', 'foo'
) ведет одинаково.lega
30.10.2015 20:02(true, 'isolate', 'foo') ведет одинаково.
— Если scope не указан, то используется родительский scope.
— Если true (или любое другое, что приводится к true, например 'foo'), тогда создается дочерний scope где родитель = прототип, т.е. все данные/методы родителя доступны в новом scope.
— Если 'isolate' тогда, создается дочерний, чистый scope, где родитель в scope.$parent, в самом scope не видно данных и методов родителя.
— Если 'root', то создается отдельный (root) scope, и «монтируется» к родительскому scope, при этом родитель в scope.$parent
Andchir
30.10.2015 13:18выполняется минимум один-два раза
Как-то очень туманно. Ценность такой информации под вопросом. Сам я всегда проверяю сколько раз выполняются фильтры и т.п. Выполняется столько сколько нужно, никаких скрытых повторных выполнений не встречал.Deamon87
30.10.2015 15:23Это зависит от криворукости написанного приложения.
Если прочитать доку, то там будет сказано, что ватчер проверяется минимум два раза. Вторая проверка делается для того, чтобы обыграть случай, когда после первого прохода один из ватчеров изменяет переменную, находящуюся под этим или другим ватчем.lega
30.10.2015 16:42+1ватчер проверяется минимум два раза
Это так, хотя зачастую достаточно и одного прохода, например ватчи которые создаются декларативным {{биндингом}} или если подключить контрол input — в них ватчи используется только для отслеживания изменений, и они не меняют модель (в input-e ватч не меняет модель, это делает директива вне ватча). А раз они не меняют модель, то и второй проход $digest не нужен, именно так и работает в Angular Light — если сделать форму с input и {{биндингом}}, то видно что при вводе текста происходит только один проход.
CepbluBoJlk
30.10.2015 21:52Эм и все? Где «рабочие примеры»? Рассказали бы об one-time bindings (::), которые появились в 1.3, позволяет значительно повысить перфоманс, уменьшив количество вотчеров.
zapolnoch
31.10.2015 08:04Добавил про one-time binding и о UI в целом.
Bronx
01.11.2015 12:50+1Вот только «one-time» == «одноразовый» («однократный»), а не «односторонний». Одностороннее связывание — это one-way binding.
Например, ng-bind=«vm.text», {{vm.text}}, ng-bind="::vm.text", {{::vm.text}} — это всё односторонние связки (от модели к представлению, но не наоборот), но из них только две последних — одноразовые.
Visions
01.11.2015 18:46Коррективы:
1. В вашем примере:
$scope.myVar = function() {
return 2 * 2;
}
$scope.$watch('myVar', function() {
alert('myVar has changed!');
});
myVar — будет отслеживаться по «ссылке» на функцию а не по возвращаемому значению!
для вашей цели нужно писать:
$scope.$watch(myVar, ....); // без кавычек
либо
$scope.$watch('myVar()', ....); // указывать вызов функции
2. При «stateful» фильтр (а он по умолчанию такой) в digest-цикле проверяется только значение аргументов и не выполняется расчет фильтра, если они остались неизменными.
Visions
01.11.2015 19:04Еще хорошее средство для сокращения числа «вотчеров» в ngRepeat:
1. сделать «директиву-обертку» для каждого элемента списка, в параметрах, которого вместо (к примеру 10 параметров) передается объект содержащий эти параметры — это позволяет уменьшить количество «вотчеров» в разы ( сравнимое с количеством объединенных в объект параметров).
2. для тех параметров из объекта, значение которых нужно отслеживать ставить внутри этой директивы «вотчер» с запуском $scope.$digest вместо $scope.$apply, а порядок запуска регулировать c помощью $timeout( fn, 0, false) — это в ряде случаев с множественными ngRepeat — позволяет еще в разы уменьшить количество обходов «вотчеров» при частных обновлениях
Еще для своих проектов писали ряд директив автоматически делающих это, плюс запуск «вотчеров» «по требованию» ( по событию ) — тоже очень здорово разгружает когда редкие обновления или ненужно двустороннее связывание
ilinsky
'myVar' будет высчитываться только один раз, а вот проверка изменений $scope.myVar будет происходить, конечно, в каждом цикле.
zapolnoch
Согласен. Исправил и поменял пример.
Houston
Теперь $watch будет следить за ссылкой на функцию, а не за значением её выполнения. Тогда уж надо
xskif
нет, не за ссылкой) все верно в примере написано.
xskif
только кавычки лишние.