Советы по производительности AngularJS

Это перепечатка статьи

Релиз Angular.js 2.0 приближается, а проблемы с производительностью первой версии все еще остаются. Эта статья посвящена оптимизации Angular.js приложений и будет полезна как начинающим, так и тем, кто уже использует этот фреймворк, но еще не сталкивался с проблемами его производительности.

Немного простой теории

Как известно, Angular.js это декларативный фронт-енд фреймворк, предоставляющий удобный байндинг данных. Конечно, не будем забывать так же о тестируемости, поддерживаемости и условной читабельности Angular.js приложений, но, в контексте этой статьи, это не важно.

Итак, одна из особенностей этого фреймворка — удобный байндинг данных «прямо из коробки». Однако за счет чего он работает? Если упрощенно, то связывание данных в Angular.js держится на scopedigest, и watcher.

Scope (или $scope) это объект, содержащий данные и/или методы, которые необходимо будет отобразить или использовать на странице, а так же ряд технических свойств, таких как его идентификатор, ссылка на родительский scope и так далее.

Watcher это объект, хранящий в себе значение заданного нами выражения и колбек функцию, которую нужно вызвать если это выражение изменится. Массив watcher-ов находится в $scope.$$watchers.

Digest — поочередный обход всех watcher-ов и вызов колбек функций тех, значение которых изменилось. Если в результате дайджеста, хотя бы одно значение было изменено, дайджест будет запущен еще раз. Поэтому часто дайджест запускается два и больше раз. Если дайджест будет запущен более 10 раз — Angular выбросит исключение.

Watcher-ы хранятся в scope и посмотреть их можно перебрав $scope.$$watchers. В основном они создаются автоматически, однако их можно создать и вручную. Директивы используют либо scope контроллера, либо создают свой. Соответственно watcher-ы директив стоит искать в их scope.

Очевидно, чем больше watcher-ов, тем дольше длится цикл дайджеста. А, поскольку, javascript язык однопоточный, то при значительной продолжительности дайджеста, приложение начнет «тормозить». Тем более что дайджест это не просто обход массива, но и вызов колбеков для тех выражений, значение которых поменялось. Считается, что angular гарантирует беспроблемную работу до тех пор, пока страница содержит до, примерно, двух тысяч watcher-ов. И, хотя эта цифра звучит достаточно внушительно, достигнуть её можно достаточно быстро.
Например вот этот маленький кусочек разметки на десять строк создаст восемьдесят watcher-ов плюс десять отдельных scope

 

А теперь к практике

Первая проблема с производительностью лежит в плоскости количества watcher-ов. И чтобы ее решить, мы должны четко понимать, что создавая любое выражение привязки, мы создаем watcher. Ng-bind, ng-model, nd-class, ng-if, ng-hide и так далее — все они создают объект-наблюдатель. И, если, в одиночку они не представляют угрозы, то их использование вместе с ng-repeat, как видно в примере выше, способно очень быстро собрать целую армию маленьких убийц нашего приложения. А самую большую опасность представляет полностью динамические таблицы. Именно они способны плодить watcher-ов в масштабах, достойных г-на Исиро Хонды.

Поэтому, первый (а иногда и последний) шаг в оптимизации количества watcher-ов лежит в анализе изменяемости данных, которые отображаются на странице. Другими словами, стоит следить только за теми данными, которые должны измениться. Очень часто возникает ситуация, когда данные нужно просто вывести пользователю. Например, нужно отобразить список всех возможных покупок, или вывести статичный текст, на который ни пользователь, ни сервер влиять не будут. Поэтому, в angular 1.3 появился специальный синтаксис для одноразовой привязки данных, который выглядит так:

data-ng-bind=»::model.name»

или так

data-ng-repeat=«model in ::models»

Это выражение означает, что, как только данные будут посчитаны и выведены на страницу, watcher отвечающий за это выражение будет удален. Комбинируя одноразовую привязку с ng-repeat можно получить существенную экономию watcher-ов в нашем приложении. Правда здесь есть один нюанс. Если данных, участвующих в выражении привязки нет (например сервер прислал null вместо названия товара), то watcher удален не будет. Он будет «ждать» данные, и только потом удален.

Второй шаг заключается в разделении ответственности. Другими словами — далеко не все должно быть Angular. В примере выше была использована директива ng-class для установки CSS классов четным и не четным строкам. Заменив ее на CSS правило tr:nth-child(even), мы избавимся от лишних watcher-ов, к тому же получим выигрыш (крайне малый) в быстродействии. Аналогичная ситуация и с событиями, такими как ng-mouseover и ng-mouseleave (их использование вызывает и другие проблемы с производительностью — о чем ниже). Зачастую их обработку можно возложить на свою директиву плюс jquery. Кстати о jQuery и директивах. Иногда, таблицу или список следует перерисовать только в одном или двух случаях. В таком случае намного эффективнее будет использовать свою директиву вместе с одним или двумя вручную созданными watcher-ами. Если какая-либо функциональность не вызывает перерисовку данных модели — это первый признак того, что ее можно сделать не в Angular style. Это не всегда нужно, но решение должно приниматься осознано.

Приведу упрощенный пример. Пускай у нас есть два списка товаров — доступные, и те, которые выбраны пользователем. Очевидно, что первый список у нас будет, во-первых, большим, а, во-вторых, статичным, так как после его загрузки, ни пользователь, ни сервер его менять не будут. Значит, здесь мы можем использовать «одноразовый» ng-repeat. А вот второй список динамичен и постоянно изменяется пользователем. Поэтому здесь нам использовать одноразовую привязку данных не стоит. Хотя, если актуальные данные нам нужны не каждую секунду, а только в момент нажатия на кнопку «купить», то здесь тоже можно сделать статику, возложив ответственность за сбор финальных данных на директиву. Нужно ли тратить ресурсы на такую оптимизацию — смотрите по текущей ситуации и размерам списков.

И, наконец, третий шаг заключается в правильном сокрытии неиспользуемой разметки. Из коробки, Angular.js предоставляет ng-show/ng-hide которые прячут или показывают нужные нам части страницы. Однако, связанные watcher-ы никуда не исчезают и участвуют в дайджесте, как и прежде. А вот использование ng-if полностью вырезает элементы из Dom-дерева вместе с соответствующими watcher-ами. Правда удаление элементов из Dom тоже не самая быстрая процедура, так что использовать ng-if стоит к тем частям разметки, которые будут скрываться/показываться не слишком часто, где «слишком» — зависит от конкретного приложения.

Итак, с множественными watcher-ами мы немного разобрались. Но долгий дайджест не единственный камень, о который может споткнуться наше приложение.

Вторая, хотя и меньшая проблема с производительностью заключается в слишком частом вызове дайджеста.

Обычно, проблемы с дайджестом возникают, когда количество watcher-ов приближается к критическому, но слишком частый запуск дайджеста может так же создать проблемы. Как известно, дайджест, это не только обход массива watcher-ов, но и выполнение колбеков для изменившихся выражений. К тому же, часто дайджест запускается несколько раз подряд, еще более замедляя производительность. Ng-model будет запускать дайджест после каждой введенной буквы. Например ввод этого слова из пятидесяти пяти букв Тетрагидропиранилциклопентилтетрагидропиридопиридиновые запустит дайджест минимум сто десять раз. Как только пользователь введет первую букву будет запущен дайджест. Поскольку в процессе его исполнения будет обнаружено, что данные модели изменились, дайджест будет выполнен повторно. Кстати, дайджест будет вызван не только на scope контроллера, а и на других scope страницы. Поэтому ng-model может стать довольно серьезной проблемой.

Простым решением, будет добавить debounce-параметр который отложит вызов дайджеста на указанное время. Аналогичная ситуация с использованием ng-mouseenter, ng-mouseover и так далее. Они могут запускать дайджест слишком часто, что приведет к падению производительности приложения.

Поэтому нужно уделять особое внимание участкам, с помощью которых пользователь может (пусть и не явно) вызывать дайджест в приложении, таких как input, области наведения и так далее. А если вам нужно вызвать дайджест вручную, старайтесь это делать тогда, когда внесено максимальное количество изменений, что бы дайджест подхватил их все за один проход.

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

  • По возможности используйте ng-bind вместо {{}}. Строковое выражение привязки обрабатывается примерно в два раза медленнее по сравнение с ng-bind. К тому же использование ng-bind избавляет от необходимости использования ng-cloak.
  • Избегайте использования сложных функций в выражениях привязки. Функции, указанные в выражении привязки запускаются каждый раз при запуске дайджеста. А, поскольку, дайджест часто запускается неоднократно, выполнение этих функций может существенно замедлить рендер страницы.
  • Используйте фильтры только в том случае, если обойтись без них нельзя. Если функции, указанные в выражении привязки выполняются один раз за дайджест, то функция фильтра выполняется два раза за дайджест, для каждого выражения. Лучше всего фильтровать данные в контроллере или сервисе.
  • По возможности, используйте $scope.$digest() вместо $scope.$apply(). Дело в том, что первая функция запустит дайджест только в пределах скоупа, на котором она была вызвана, а вторая — на всех scope, начиная с rootScope. Очевидно, что первый дайджест пройдет быстрее. Кстати $timeout в конце вызовет именно $rootScope.$apply().
  • Помните о возможности отложенного вызова дайджеста на вводе пользователя, задавая параметр debounce: data-ng-model-options=»{debounce: 150}»
  • Старайтесь избегать использования ng-mouse-over и подобных директив. Вызов этого события запустит дайджест, а природа таких событий такова, что они могут быть вызваны многократно за короткий промежуток времени.
  • Создавая свои watcher-ы, не забывайте сохранять функцию их удаления и вызывать ее сразу, как только watcher-ы перестанут быть нужны. Кроме того, избегайте установки флага objectEquality в true. Это вызывает глубокое копирование и сравнение нового и старого значений для определения необходимости вызова колбек функции.
  • Не стоит хранить ссылки на Dom элементы в scope. Он содержат ссылки на родительский и дочерние элементы, т.е. по сути, на весь дом элемент. А, значит, дайджест будет пробегать по всему Dom дереву проверяя какой из объектов поменялся. Не стоит говорить, насколько это затратно.
  • Пользуйтесь параметром track by в директиве ng-repeat. Во первых это быстрее, а во вторых убережет от ошибки duplicates in a repeater are not allowed , которая возникает, когда мы пытаемся вывести одинаковые объекты в списке.

На этом статья закончена. Более подробно можно почитать по ссылкам ниже:

AngularJS Performance Tips
AngularJS: 6 Common Pitfalls Using Scopes
Speeding up AngularJS apps with simple optimizations
AngularJS — Overcoming performance issues.

Оригинал статьи — http://m.habrahabr.ru/company/infopulse/blog/262389/

LEAVE A COMMENT