Многие статьи об оптимизации производительности, в первую очередь пытаются «заглянуть под капот» Angular и перегружают читателя информацией о внутренней организации фреймворка. Знакомство с внутренними механизмами работы очень важно, но в данной статье я попытался собрать самые простые примеры, которые оказывают наибольшее влияние на производительность приложения и помогают максимально быстро решить типичные проблемы.

Чем измерять?


Измерять производительность приложения удобно расширением 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)


  1. ilinsky
    30.10.2015 12:20
    +1

    $scope.myVar = 2 * 2;
    $scope.$watch('myVar', function() {
    	alert('myVar has changed!');
    });
    

    'myVar' будет высчитываться минимум один-два раза при каждом цикле $digest.

    'myVar' будет высчитываться только один раз, а вот проверка изменений $scope.myVar будет происходить, конечно, в каждом цикле.


    1. zapolnoch
      30.10.2015 12:33

      Согласен. Исправил и поменял пример.


      1. Houston
        30.10.2015 15:36

        Теперь $watch будет следить за ссылкой на функцию, а не за значением её выполнения. Тогда уж надо

        $scope.$watch(function() {
           return $scope.myVar();
        }, function() {
           alert('myVar has changed!');
        });
        


        1. xskif
          30.10.2015 20:59

          нет, не за ссылкой) все верно в примере написано.


          1. xskif
            30.10.2015 20:59

            только кавычки лишние.


  1. 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мс (для простых ватчей).


    1. NikitaKA
      30.10.2015 14:33
      +2

      Так же не сказано про самую главную «оптимизацию» — компонентный подход

      Не могли бы вы привести пример?


      1. 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 в один блок, тем самым он нагружает все остальные блоки, когда во втором примере этого не происходит.

        Так же такие элементы как верхнее меню, выскакивающие диалоги, блоки с рекламой, «Популярное за сутки» и пр. никак не связаны с комментариями и главным контентом страницы, все их имеет смысл делать самостоятельными компонентами, а не «кусочком» монолитного приложения. Это не только «ускорят» приложение но и хорошо для архитектуры приложения, имхо.


        1. sAntee
          30.10.2015 16:47

          я не знаю, что там происходит с Angular Light, но в обычном Angular'е, никаких дополнительных $rootScope на каждую директиву не создается. $rootScope это вообще сервис, который по определению синглтон в ангуляре. Isolated scope это другое. В производительности можно выиграть, если вызывать $digest на этом скоупе, вместо $apply ($rootScope.$digest). Но это может быть не всегда возможно.


          1. lega
            30.10.2015 17:27
            +1

            Angular'е, никаких дополнительных $rootScope на каждую директиву не создается
            Штатно не создается, но можно сделать новый rootScope вручную (не через сервис), и у него будет свой $digest цикл. Правильно это или нет, каждый принимает решение за себя, но некоторые это используют.


            1. sAntee
              31.10.2015 11:22

              А можно пример как это сделать?


              1. lega
                31.10.2015 11:47

                Например так:

                newRootScope = new $rootScope.constructor()
                

                Хотя я видел более красивый вариант.


                1. sAntee
                  31.10.2015 17:32

                  А что это дает в сравнении со $scope.$new() с вызовом $digest на нем? В чайлдовых директивах и темплейтах все равно будет использоваться то что возвращает $injector сервис. Вся целостность приложения ломается. Либо я чего-то не понимаю.


                  1. lega
                    31.10.2015 23:12

                    Вот вариант со своими инжектами.


                    1. sAntee
                      31.10.2015 23:41

                      да вот только это три отдельных приложения.


                      1. lega
                        01.11.2015 11:37

                        Главное работает, дальше можно сделать красивые обертки и использовать.
                        А вообще, апи у Angular 1 в этом плане не удачное, и то что в Angular 2 делают по другому — это как подтверждение.


                        1. sAntee
                          01.11.2015 12:11

                          Черт, естественно оно работает, чего б ему не работать :) Вот только

                          Вся целостность приложения ломается

                          Как вы в таком случае намереваетесь налаживать какое-то взаимодействие между этими компонентами? И, главное, зачем так извращаться? Если приходиться это делать чтобы сократить количество watcher'ов, то тут проще вообще от data-binding'а в шаблонах избавиться, чем по инстансу приложения создавать на контрол. Про удачность апи это да.


                        1. sAntee
                          01.11.2015 12:59

                          В общем ок, предлагаю согласиться на том, что все ждем 2-й ангуляр :)


                          1. lega
                            01.11.2015 22:44

                            Как вы в таком случае намереваетесь налаживать какое-то взаимодействие между этими компонентами?
                            Я не намереваюсь, я использую Angular Light и у меня таких проблем нет.
                            Про удачность апи это да. ждем 2-й ангуляр :)
                            Ангуляр 2 ещё не зарелизился, а в нем уже не все хорошо как хотелось бы, хотя фанатам буде плевать. Так же кроме Ангуляра есть полно других фреймворков (и не плохих тоже).


                            1. sAntee
                              01.11.2015 23:23

                              Я не намереваюсь, я использую Angular Light и у меня таких проблем нет.

                              Ну так спорили же за обычный. Идеальных фреймворков вообще не бывает, посмотрим чего они там намудрят…


        1. NikitaKA
          30.10.2015 17:02

          Спасибо за ценный комментарий.

          Подскажите, пожалуйста, по аргументам в ваших примерах: scope:'root'/'isolate' — в документации написано, что аргументы могут быть true/'isolate', но в ваших примерах используется 'root', который ведет себя как-то по-особенному, а все остальное (true, 'isolate', 'foo') ведет одинаково.


          1. lega
            30.10.2015 17:25

            Вот новая дока (с главной страницы на неё ссылка).
            А вообще почти все параметры для директив — это просто ф-ии, и можно добавить своих аттрибутов, вот тот самый «обработчик» scope или в js виде.


          1. 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


  1. Andchir
    30.10.2015 13:18

    выполняется минимум один-два раза
    Как-то очень туманно. Ценность такой информации под вопросом. Сам я всегда проверяю сколько раз выполняются фильтры и т.п. Выполняется столько сколько нужно, никаких скрытых повторных выполнений не встречал.


    1. Deamon87
      30.10.2015 15:23

      Это зависит от криворукости написанного приложения.
      Если прочитать доку, то там будет сказано, что ватчер проверяется минимум два раза. Вторая проверка делается для того, чтобы обыграть случай, когда после первого прохода один из ватчеров изменяет переменную, находящуюся под этим или другим ватчем.


      1. lega
        30.10.2015 16:42
        +1

        ватчер проверяется минимум два раза
        Это так, хотя зачастую достаточно и одного прохода, например ватчи которые создаются декларативным {{биндингом}} или если подключить контрол input — в них ватчи используется только для отслеживания изменений, и они не меняют модель (в input-e ватч не меняет модель, это делает директива вне ватча). А раз они не меняют модель, то и второй проход $digest не нужен, именно так и работает в Angular Light — если сделать форму с input и {{биндингом}}, то видно что при вводе текста происходит только один проход.


  1. CepbluBoJlk
    30.10.2015 21:52

    Эм и все? Где «рабочие примеры»? Рассказали бы об one-time bindings (::), которые появились в 1.3, позволяет значительно повысить перфоманс, уменьшив количество вотчеров.


    1. zapolnoch
      31.10.2015 08:04

      Добавил про one-time binding и о UI в целом.


      1. Bronx
        01.11.2015 12:50
        +1

        Вот только «one-time» == «одноразовый» («однократный»), а не «односторонний». Одностороннее связывание — это one-way binding.

        Например, ng-bind=«vm.text», {{vm.text}}, ng-bind="::vm.text", {{::vm.text}} — это всё односторонние связки (от модели к представлению, но не наоборот), но из них только две последних — одноразовые.


  1. 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-цикле проверяется только значение аргументов и не выполняется расчет фильтра, если они остались неизменными.


  1. Visions
    01.11.2015 19:04

    Еще хорошее средство для сокращения числа «вотчеров» в ngRepeat:
    1. сделать «директиву-обертку» для каждого элемента списка, в параметрах, которого вместо (к примеру 10 параметров) передается объект содержащий эти параметры — это позволяет уменьшить количество «вотчеров» в разы ( сравнимое с количеством объединенных в объект параметров).
    2. для тех параметров из объекта, значение которых нужно отслеживать ставить внутри этой директивы «вотчер» с запуском $scope.$digest вместо $scope.$apply, а порядок запуска регулировать c помощью $timeout( fn, 0, false) — это в ряде случаев с множественными ngRepeat — позволяет еще в разы уменьшить количество обходов «вотчеров» при частных обновлениях

    Еще для своих проектов писали ряд директив автоматически делающих это, плюс запуск «вотчеров» «по требованию» ( по событию ) — тоже очень здорово разгружает когда редкие обновления или ненужно двустороннее связывание