[Перевод] Главные причины медленной работы Angular-приложений
Angular — это быстрый фреймворк. Он даёт разработчикам обширные возможности по улучшению производительности за счёт тонких настроек. Правда, программистам практически никогда не требуется делать что-то особенное для того, чтобы создавать чрезвычайно производительный код, работая над обычными приложениями.
Но оказывается, что в некоторых особенных случаях проблемы с производительностью Angular-приложений всё же могут возникнуть. Во-первых — при разработке приложений, которые должны быть чрезвычайно производительными. Во-вторых — если это приложения, работающие с большими объёмами сложного контента. В-третьих — в приложениях, содержимое которых очень часто обновляется.
По поводу улучшения производительности Angular-приложений написано уже очень много всего. В бесчисленных публикациях даётся масса советов. И хотя большинство из этих советов могут принести пользу тому, кто их применит, автор статьи, перевод которой мы сегодня публикуем, говорит, что те проблемы, с которыми он столкнулся, обсуждаются не особенно часто.
Этот материал посвящён разбору основных причин того, что Angular-приложения становятся медленными по мере роста их масштабов. При этом приведённые здесь советы можно будет применить при разработке крупных проектов на любом фреймворке, а не только на Angular.
Имеют ли значение микрооптимизации?
Опыт подсказывает мне, что микрооптимизации — это тема, которую разработчики очень часто понимают неверно. Когда бы я ни пытался справиться с проблемами, касающимися производительности, многие мои коллеги указывали на код, который мог бы привести к этим проблемам. В качестве методики решения проблем мне предлагали микрооптимизации, которые могли бы помочь сэкономить лишь несколько миллисекунд и не более того.
Вот что мне доводилось слышать:
- «Мы используем слишком много методов
reduce
,map
иfilter
. Давайте заменим их все на циклыfor
!». - «Давайте, чтобы увеличить скорость доступа к данным, воспользуемся словарём!».
- «Нам нужны битовые операторы!».
Я всегда думал, что дело вовсе не в этом.
Но всё вышесказанное, конечно, надо принимать во внимание при разработке высокопроизводительных приложений, фреймворков и библиотек. Первое, что разработчики обычно делают, пытаясь найти причины «тормозов», заключается в анализе производительности функций:
- «Сколько времени займёт поиск элемента в этом списке [или, может быть, 300 элементов]?».
- «Как долго будет выполняться сортировка [800 элементов]?».
Но при разработке обычного приложения эти оптимизации могут значить гораздо меньше, чем может показаться на первый взгляд.
Я не говорю, что подобные вещи вовсе неважны, но я начал бы поиск причин проблем с производительностью приложения со следующих вопросов:
- Как много информации приложение обычно выводит на экран?
- Как часто фреймворк выполняет повторный рендеринг компонентов?
Ниже мы увидим, что даже тот, кто следует рекомендованным подходам к разработке, не всегда застрахован от плохой производительности приложений. И чаще всего причиной проблем является не используемый фреймворк, а недочёты кода или архитектуры проекта.
Приложение слишком часто выполняет операции рендеринга
Начнём с одной весьма распространённой проблемы: приложение без необходимости выполняет повторный рендеринг компонентов, что делает проект медленнее, чем он мог бы быть. Эту проблему несложно решить, но и возникает она тоже без особых «усилий» со стороны разработчика.
▍Изменение способа обнаружения изменений
Установка настройки, отвечающей за стандартный механизм обнаружения изменений, в значение OnPush
— это почти обязательный шаг, выполняемый в том случае, если приложение страдает от недостатка производительности, или в ситуации, когда нужно защититься от возникновения проблем с производительностью в будущем.
Настроив компоненты так, чтобы они обновлялись только при возникновении реальной необходимости, можно предотвратить повторный рендеринг компонентов в случаях, когда данные, выводимые ими, не поменялись. Это — ясная стратегия работы с изменениями, использование которой значительно упрощается при применении наблюдаемых объектов и асинхронных пайпов.
▍Асинхронные пайпы
Даже если вы используете в шаблоне стратегию обнаружения изменений OnPush
и асинхронные пайпы, приложение всё ещё может выполнять рендеринг чаще, чем нужно.
Например, если сделать так, чтобы наблюдаемые объекты не выдавали бы данные без необходимости, это убережёт компоненты от повторного рендеринга. Тут можно воспользоваться такими RxJS-операторами, как filter
и distinctUntilChanged
для того, чтобы полностью избавиться от операций повторного рендеринга.
Ещё одна проблема, с которой я сталкивался даже тогда, когда пользовался наблюдаемыми объектами и асинхронными пайпами, заключалась в выборе элементов из хранилища с использованием селекторов. Если мы пишем и используем очень точные селекторы, то мы получаем обновления только от выбранного среза состояния.
Если же выбрать весь объект из дерева состояния Redux, то селекторы будут реагировать на каждое изменение дерева. В результате будут вызываться обновления компонентов, которые, на деле, не затронуты этими изменениями.
Это небольшое, как может показаться, изменение позволило мне довести одно из моих приложений в IE 11 от состояния, когда им едва можно было пользоваться, до состояния, когда оно начало показывать довольно хорошую производительность. Вот один из моих материалов, посвящённых RxJS.
▍Обновления, выполняемые слишком часто
Очень частые обновления страниц — это не та область, в которой Angular показывает себя наилучшим образом. Вероятно, дело тут в библиотеке Zone.js, которая, кроме того, лежит в основе автоматического механизма обнаружения изменений Angular.
Zone.js выполняет обезьяньи патчи всех событий и планирует запуск обнаружения изменений на моменты возникновения этих событий. Это означает, что если приложение на высоких скоростях выдаёт в потоковом режиме события (события WebSocket или даже события DOM), то для каждого полученного события Zone.js запустит процесс обнаружения изменений. В подобных случаях перед нами, определённо, открываются возможности для улучшений производительности системы.
Вот мой материал, посвящённый повышению производительности Angular-приложений путём избавления от Zone.js.
Конечно, для решения этой проблемы вам не нужно убирать Zone.js из своего приложения. Вот несколько шагов, которые можно предпринять вместо этого:
- Отключите компоненты, которые обновляются слишком часто, после чего, при поступлении уведомлений от подписок, обновляйте их, обращая внимание на каждую деталь.
- Используйте
ngZone.runOutsideAngular(callback)
для вызова коллбэка за пределами системы обнаружения изменений Angular. - И последнее средство — исключите события из состава тех, для которых Zone.js создаёт обезьяньи патчи.
Приложение рендерит слишком много компонентов
Вне зависимости от того, насколько быстр ваш фреймворк, вы, если за один приём рендерите тысячи сложных компонентов, попадёте в ситуацию, когда браузер начнёт, в той или иной мере, «подтормаживать».
Даже если на вашем Macbook Pro это и не очень заметно, на более медленных машинах подобное может проявиться весьма ярко. Разработчику стоит учитывать то, что не все его пользователи сидят на мощном «железе».
Очень важно так построить приложение, чтобы даже компоненты, выводящие множество элементов (например — компоненты-списки), были определённым образом оптимизированы.
Как решить эту проблему?
▍Использование ключей
Использование ключей — это самая простая и, возможно, самая известная методика ускорения рендеринга списков, которая применяется в большинстве библиотек. В основе этой методики лежит простая идея: мы назначаем каждому элементу списка ключ, а библиотека выполняет повторный рендеринг элемента только в том случае, если его ключ изменился.
Этот подход к оптимизации рендеринга отлично показывает себя при вставке и удалении элементов списков, или тогда, когда число изменённых элементов ограничено. Но он не решает проблем с производительностью в случаях, когда нужно в один момент вывести огромное количество элементов. Например — если мы рендерим очень длинный список при загрузке страницы.
▍Виртуальный скроллинг
Виртуальный скроллинг подразумевает рендеринг только того, что видит пользователь.
У этого подхода к оптимизации рендеринга есть особенности, которые нужно учитывать для обеспечения доступности данных и удобства работы с ними. Но это — один из лучших методов улучшения воспринимаемой пользователем производительности страниц. Он помогает избежать «заморозки» страниц на чрезмерно большие периоды времени. Время «заморозки», которое плохо влияет на её восприятие пользователем, меньше, чем можно подумать.
Виртуальный скроллинг весьма легко реализуем. Соответствующие инструкции и инструменты можно найти в документации к Angular CDK.
▍Асинхронный рендеринг
Это — довольно старая методика оптимизации рендеринга, хотя я предпочёл бы ей виртуальный скроллинг. Но это — всё же лучше, чем одновременный рендеринг 1000 элементов. Кроме того, асинхронный рендеринг очень легко реализовать. Для этого не придётся писать больших объёмов кода.
В основе асинхронного рендеринга лежит следующая идея: нужно начать рендеринг ограниченного количества элементов (например, если в списке 500 элементов — начать рендерить 50). Затем надо запланировать рендеринг следующих 50 элементов, используя setTimeout(0)
. Планировать рендеринг очередных порций элементов нужно до тех пор, пока они все не будут выведены. Это — простая методика оптимизации, но благодаря её использованию браузер не окажется «замороженным» на сотни миллисекунд во время вывода страницы.
▍Ленивый рендеринг
Не все материалы страниц нужно рендерить сразу. Иногда можно просто вывести компонент тогда, когда пользователю понадобится с ним работать.
Однажды я попал в ситуацию, в которой мне это пригодилось. Я тогда работал над страницей, которая использовала множество экземпляров Quill — знаменитой WYSIWYG-библиотеки.
Quill — это отличный, но довольно «тяжёлый» инструмент. Создание экземпляра одного из его компонентов занимает 20–30 мс, а на странице мне нужны были сотни таких компонентов. Мой Macbook Pro с этой задачей не справился.
Попытка за один заход создать все эти компоненты была довольно-таки глупой затеей. Ключ к решению проблемы заключался в том, что WYSIWYG-область, когда с ней не взаимодействуют, могла бы представлять собой простой фрагмент HTML-разметки. Я мог создать экземпляр компонента тогда, когда он понадобится пользователю, то есть, например, когда на соответствующую область страницы наводили указатель мыши, или тогда, когда по ней щёлкали. После того, как я применил в этом проекте методику ленивого рендеринга, все проблемы с производительностью исчезли.
▍Ленивые прослушиватели событий
Эта методика рендеринга напрямую связана с той, которую мы только что обсуждали. А именно, подписка на слишком большое количество событий и прослушивание этих событий могут оказаться чрезвычайно ресурсозатратными.
Для того чтобы избежать опасности подписки на слишком большое количество событий, можно выбрать один из вариантов действий:
- Если у вас имеется большой список элементов с обработчиками событий DOM, сделайте так, чтобы подписка на события оформлялась бы только для видимых элементов (тут вам на помощь может прийти виртуальный скроллинг).
- Иногда лучше создать только одно глобальное событие в пределах сервиса, вместо того, чтобы подписываться на событие в каждой директиве или в каждом компоненте.
Какой-то код… просто может быть медленным
Если вы исследовали приложение и выяснили, что оно не рендерит слишком много компонентов и не обновляет их слишком часто, это значит, что сам ваш код может быть достаточно медленным. Возможно, это связано с какими-нибудь тяжёлыми вычислениями и вообще не имеет отношения к DOM.
Но это — не повод для уныния. Это даже хорошо, так как в наши дни существуют эффективные инструменты для решения подобных проблем:
- Используйте веб-воркеры. Их, кстати, можно очень быстро создавать с помощью Angular CLI. Когда можно применить такой подход к решению проблемы «тяжёлого» кода? Тут всё очень просто: использовать веб-воркеры стоит тогда, когда код не взаимодействует с DOM, и когда на его выполнение уходит некоторое время. Обычно в таком коде выполняются какие-то расчёты, обработка данных и прочее подобное. Может показаться, что веб-воркеры должны хорошо сочетаться с Redux, но пока ещё время их совместного использования не пришло.
- Применяйте WebAssembly. Например — посредством AssemblyScript. Подробнее эта идея освещена здесь.
Если эта пара идей вам не нравится, или они просто не способны решить вашу проблему, это значит, что пришло время микрооптимизаций и анализа того, как они могут улучшить производительность системы:
- Реализуйте собственный IterableDiffer.
- Избавьтесь от
filter
,reduce
иmap
, переведите всё на циклыfor
. В циклах, для уменьшения количества итераций, используйтеbreak
иcontinue
. - Следите за тем, чтобы ваши объекты имели бы правильную форму. Тут вам будет полезно посмотреть это видео, в котором речь идёт о том, почему Angular так быстр.
Итоги
Мы обсудили различные подходы к ускорению Angular-приложений. Подведём итоги:
- Отойдите от стандартных настроек фреймворка: применяйте стратегию обнаружения изменений
ChangeDetection.OnPush
и используйтеtrackBy
при работе с массивами. - Постарайтесь, чтобы операции рендеринга выполнялись бы не так часто, как обычно, ответственно подойдя к запуску операций по обнаружению изменений. Если нужно — выполняйте код за пределами воздействия Zone.js.
- Постарайтесь рендерить меньше компонентов, используя различные подходы, вроде виртуального скроллинга и ленивого рендеринга.
- Не используйте слишком много прослушивателей событий. Оформляйте подписки только на события видимых элементов, подписывайтесь только на один глобальный прослушиватель событий.
Уважаемые читатели! Как вы ищете и решаете проблемы с производительностью, сталкиваясь с медленной работой Angular-приложений?