Оптимизация

f

Почему 90% советов по оптимизации Angular — пустой звук

Если вы когда-нибудь искали способы ускорить Angular-приложение, то наверняка натыкались на общие фразы вроде «используйте OnPush» или «делайте lazy loading». Проблема в том, что без понимания, как именно работают внутренние механизмы Angular, эти советы превращаются в магические заклинания — вроде вроде бы сделали, а эффекта нет.

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

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

Change Detection: как не выстрелить себе в ногу с OnPush

Стратегия OnPush (ChangeDetectionStrategy.OnPush) — один из самых популярных инструментов оптимизации. Но её неправильное использование — одна из главных причин падения производительности в Angular-приложениях.

OnPush работает только в двух сценариях: когда изменилась входная ссылка на объект (@Input) или когда в компоненте или его потомке произошло событие (например, клик). Если вы передаёте в компонент массив и мутируете его — добавляете элемент через push — ссылка останется той же, и Angular не поймёт, что данные обновились.

Профессионал всегда использует иммутабельные операции — spread-оператор или метод concat/map, возвращающие новый массив. Или, альтернативно, триггерит ChangeDetectorRef.markForCheck() вручную после мутации. Это не «костыль», а осознанный выбор.

Три скрытые утечки памяти, которые убивают производительность

Когда говорят об утечках памяти в Angular, вспоминают только забытые подписки на Observable. Но есть более коварные вещи, которые не отлавливаются стандартными тулами.

В реальных проектах я видел, как при обычной навигации между страницами накапливалось до 500 МБ «висячих» данных из-за таких утечек. Решение — всегда использовать одни и те же ссылки: выносить функции в поля компонента и передавать их как `this.method`, а для событий использовать декораторы @HostListener.

Lazy loading — это не только про маршруты

Практически каждый курс по Angular рассказывает про отложенную загрузку модулей через loadChildren в Router. Но мало кто упоминает, что lazy loading можно (и нужно) на уровне тяжелых компонентов внутри страницы.

Например, если у вас на странице есть виджет с графиком на базе D3.js — он может весить 100+ КБ. И загружать его сразу, когда пользователь только открыл страницу, нет смысла. Используйте `@defer` (начиная с Angular 17+) или библиотеки вроде ng-lazyload-image — они позволяют показать скелетон, а сам компонент с зависимостями загрузить только когда он попадает во вьюпорт.

В одном из проектов по аналитике мы таким образом сократили начальную загрузку страницы с 4 до 1,2 секунды, и это при том, что сама технология Angular у нас не менялась. Только добавили lazy loading для карточек с графиками и таблицами.

TrackBy в *ngFor — микрооптимизация или спасение?

Рекомендация «добавьте trackBy в *ngFor» звучит повсюду, но на практике мало кто понимает, как это работает и когда это критично. Angular без trackBy при каждом цикле изменения данных удаляет все DOM-элементы из списка и создаёт их заново. Если список из 100 карточек с картинками — это перерисовка всего набора картинок, что может быть очень дорого.

Но если ваш список статический (например, выводится один раз без последующего обновления), trackBy не даст никакого выигрыша — Angular и так не будет пересоздавать элементы, потому что нет триггера для проверки. Эффект trackBy проявляется, когда элементы динамически добавляются, удаляются или меняют порядок. На стандартной странице с таблицей в 500 строк, где обновляется столбец с ценами, правильный trackBy по ID может ускорить обновление в 10-20 раз.

Важный нюанс: функция trackBy должна возвращать строку или число, а не объект. Если вы возвращаете объект, Angular будет сравнивать ссылки, и вы вернётесь к поведению без trackBy.

Сборка и AOT: битва за килобайты

JIT (Just-in-Time) компиляция удобна для разработки — она быстрее запускается и показывает ошибки сразу. Но в продакшне вы обязаны переключиться на AOT (Ahead-of-Time), потому что Angular собирает шаблоны в оптимизированный JavaScript на этапе сборки, а не в браузере пользователя.

Кроме того, AOT позволяет включить strict mode в шаблонах и отсечь много лишнего кода, который Angular традиционно тащит для совместимости с HTML. В версиях Angular 15+ с standalone компонентами и ESM модулями размер сборки можно дополнительно уменьшить за счёт tree shaking.

Типичная ошибка новичков: не удалять неиспользуемые модули (например, FormsModule, если вы не используете ngModel). Angular оптимизатор не всегда может выкинуть их из бандла, если они импортированы. Каждый такой модуль — это лишние 30-50 КБ в финальном файле.

Самый недооценённый инструмент профайлинга

Все используют Chrome DevTools. Но для Angular есть специализированный инструмент — Angular DevTools, которое устанавливается как расширение. В нём есть вкладка «Profiler», которая покажет вам, сколько времени заняла проверка каждого компонента, и какие изменения вызвали перерисовку.

Большой сюрприз для многих: оказывается, что часто «тормоза» вызваны не медленными вычислениями, а тем, что в приложении слишком много подписок на один и тот же поток данных, и каждый раз Angular перепроверяет один и тот же компонент по 10 раз за цикл. Видны «двойные проверки» — типичная проблема при использовании async pipe в нескольких дочерних компонентах от одного Observable.

Решение: либо шэрить значение через switchMap и shareReplay в сервисе, либо использовать @Input и передавать готовые данные, а не сам Observable. На проектах с сотнями компонентов это даёт выигрыш в 50-100 мс на каждое взаимодействие, что для UX равнозначно «мгновенному» отклику.

Добавлено: 23.04.2026