Почему традиционный подход теряет эффективность
В крупных Angular‑проектах часто приходится получать данные в одном маршруте и передавать их через цепочку компонентов. При использовании @Input/@Output или сервисов с подписками код быстро запутывается: каждый новый уровень добавляет зависимости, а забытые отписки приводят к утечкам памяти. Кроме того, классический механизм обнаружения изменений, основанный на Zone.js, вызывает глобальное обновление дерева компонентов даже при незначительных изменениях, что ухудшает производительность и увеличивает размер бандла.
Сигналы как фундамент реактивности
Новый примитив Signals в Angular представляет собой реактивный контейнер, способный хранить значение и оповещать зависимые вычисления о его изменении. В отличие от потоков RxJS, сигналы не требуют создания Observable‑ов и подписок: достаточно вызвать функцию‑чтец, а система автоматически отслеживает зависимости. Это упрощает модель данных, делает её более предсказуемой и позволяет избавиться от «ручного» управления подписками.
Зонeless‑детекция: как работает без Zone.js
Отказ от Zone.js (зонeless‑детекция) реализуется через стабильный механизм change detection, который запускается только тогда, когда изменяется сигнал. Angular фиксирует, какие компоненты читают конкретный сигнал, и при изменении значения обновляет лишь эти узлы. Эффект сравним с переходом от «постоянного сканера» к «умному датчику»: реактивность ускоряется в три раза, а количество проверок DOM резко снижается. При этом приложение сохраняет полную совместимость с существующими шаблонами и сервисами.
Паттерны совместного использования состояния
Для организации общего состояния в приложении рекомендуется использовать один из следующих подходов:
| Паттерн | Описание | Преимущества |
|---|---|---|
| Глобальный сигнал | Создаётся в отдельном сервисе, экспортируется как функция‑чтец и функция‑записчик. | Централизованный доступ, отсутствие необходимости в BehaviorSubject. |
| Сигнальный репозиторий | Набор связанных сигналов (например, user, settings, permissions) группируется в объект‑репозиторий. | Ясная структура, возможность атомарных обновлений через batch. |
| Кастомные вычисляемые сигналы | На основе базовых сигналов создаются производные, которые автоматически пересчитываются при изменении исходных данных. | Минимум логики в компонентах, чистый поток данных. |
Во всех случаях важно использовать batch(() => { ... }) для группировки нескольких изменений в одну фазу детекции, тем самым избегая лишних перерисовок.
Сигналы vs RxJS Subjects
| Критерий | Signals | RxJS Subjects |
|---|---|---|
| Подписки | Не требуются, зависимости фиксируются автоматически | Требуется ручное управление subscribe/unsubscribe |
| Память | Минимальный overhead, нет внутренних буферов | Возможны накопления событий, если не отписаться |
| Трассировка | Явно видно, какие компоненты зависят от сигнала | Трудно отследить, кто подписан на поток |
| Производительность | Обновления только у затронутых компонентов | При каждом next может происходить глобальная проверка, если используется asyncPipe |
В небольших сценариях RxJS остаётся удобным, но при масштабных приложениях сигналы демонстрируют более чистую архитектуру и лучшую отзывчивость.
Интеграция с TanStack Query
Для работы с внешними API рекомендуется сочетать TanStack Query (ранее React Query) с сигналами. TanStack Query отвечает за кэширование, повторные попытки и синхронизацию запросов, а сигналы служат «мостом» между кэшем и Angular‑компонентами:
@Injectable({ providedIn: 'root' })
export class UserDataService {
private userSignal = signal<User | null>(null);
constructor(private queryClient: QueryClient) {
this.queryClient
.fetchQuery(['user', userId], fetchUser)
.then(user => this.userSignal.set(user));
}
get user() {
return this.userSignal;
}
}
Компонент читает userDataService.user(); при получении новых данных из кэша сигнал автоматически обновит UI без дополнительных подписок. Такой подход устраняет дублирование логики кэширования и реактивного обновления, позволяя сосредоточиться на бизнес‑логике.
Практические результаты и рекомендации
- Скорость – в бенчмарках приложение с сигнальной архитектурой демонстрирует до 30 % ускорения первого рендера и до 50 % снижения времени отклика при частых обновлениях данных.
- Размер бандла – удаление Zone.js и RxJS‑операторов сокращает загрузку скриптов на ~150 KB, что особенно ощутимо на мобильных сетях.
- Поддержка – сигналы полностью поддерживаются в текущих версиях Angular (v16+), а переход к зоне‑less детекции требует лишь небольших настроек в
ngZone: 'noop'. - Миграция – рекомендуется начать с выделения «корневых» состояний (пользователь, настройки, токены) в глобальные сигналы, затем постепенно заменить
BehaviorSubjectв сервисах на сигналы, проверяя тесты после каждой итерации. - Командные процессы – внедрение сигнальной модели упрощает ревью кода, так как каждое изменение явно видно в месте его использования, а отсутствие подписок уменьшает вероятность забытых отписок.
Сигналы и зонeless‑детекция открывают новый уровень предсказуемости и эффективности в Angular‑приложениях. Их применение в сочетании с современными инструментами кэширования, такими как TanStack Query, позволяет построить масштабируемую и быструю архитектуру, отвечающую требованиям как небольших стартапов, так и крупных корпоративных систем.