Оптимизация

Почему 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. Но есть более коварные вещи, которые не отлавливаются стандартными тулами.
- Слушатели глобальных событий — если вы подписались на window resize, scroll или document click напрямую через addEventListener, а не через @HostListener, Angular не сможет отписать их при уничтожении компонента.
- Ссылки в замыканиях — когда вы передаёте колбэк, который захватывает контекст, и этот колбэк сохраняется где-то в сервисе или замыкании, весь компонент и его зависимости не попадают в сборщик мусора.
- Создание новых ссылок на функции в шаблоне — например, использование (click)="myMethod.bind(this)" или передача стрелочной функции в параметр. Каждый цикл обнаружения изменений создаёт новый объект, который нужно собрать GC, и если таких вызовов тысячи — браузер начинает тормозить.
В реальных проектах я видел, как при обычной навигации между страницами накапливалось до 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
