05 — OnPush и zoneless: когда Angular проверяет компоненты
Это та самая тема, где все путаются. Разложим по полочкам. Точная версия с исходниками — 01-cd-strategies в соседнем vault'е.
Главная мысль: тут ДВА разных вопроса
Люди мешают в кучу два независимых вопроса. Раздели их — и всё станет ясно:
- КТО будит проверку? (что вообще запускает tick) — это про zone.js vs zoneless. Свойство всего приложения.
- КАКИЕ компоненты проверяются внутри tick? — это про Default vs OnPush. Свойство конкретного компонента.
Это как две разные ручки настройки. Одна — «как часто включается уборка», вторая — «какие комнаты убираем».
Ось 1 — кто будит проверку (zone.js vs zoneless)
zone.js (старый способ)
zone.js — это «жучок», который влезает во ВСЕ асинхронные функции браузера
(setTimeout, клики, запросы). После любого такого действия он говорит Angular:
«эй, что-то произошло, проверь всё».
Минус: проверка дёргается даже когда ничего важного не поменялось (сработал посторонний таймер — а CD всё равно прошёлся по всему дереву).
zoneless (по умолчанию в v21)
Никакого жучка. Проверку планируют только конкретные понятные вещи:
- сигнал, прочитанный в шаблоне, изменился;
- сработало событие из шаблона (
(click)); - пришёл новый
@Input; - явный
markForCheck()/AsyncPipe.
Обычный setTimeout, не трогающий сигнал, проверку не запускает. В этом вся суть.
Ось 2 — какие компоненты проверяются (Default vs OnPush)
Когда проверка уже идёт, она решает по каждому компоненту: проверять его или пропустить.
- Default — «проверяй меня всегда, когда обход дошёл».
- OnPush — «проверяй меня, только если у меня реально что-то изменилось»
(новый
@Inputпо ссылке, событие из моего шаблона, мой сигнал,markForCheck).
OnPush = «не буди меня по пустякам». Это оптимизация: тяжёлый компонент не пересчитывается зря.
Матрица 2×2 — что реально происходит
| Default | OnPush | |
|---|---|---|
| zone.js | Любое действие → проверяется ВСЁ дерево. Просто, но расточительно. | Любое действие будит проверку, но «спокойные» OnPush-ветки пропускаются. Классическая оптимизация. |
| zoneless (v21) | Проверка только от сигнала/события. Но Default сам по себе уже почти не важен. | Идеал v21: OnPush + сигналы. Сигнал точечно обновляет свой компонент. |
На бытовых примерах
zone.js + Default — «уборщик-параноик»
Кто-то чихнул в любой комнате → уборщик обходит весь дом и протирает всё. Надёжно, но много лишней работы.
zone.js + OnPush — «уборщик с правилом»
Уборщик всё равно встаёт на каждый чих, но в комнаты с табличкой «OnPush» заходит, только если там реально намусорили. Меньше работы.
zoneless + Default — «уборщик спит»
Уборщик не встаёт сам по себе. Если ты поменял обычное поле (this.x = 5) без
сигнала — он даже не узнал, экран не обновился. Это ловушка миграции!
zoneless + OnPush + сигналы — «умный уборщик» (рекомендуется в v21)
Уборка запускается только когда коробочка-сигнал реально поменялась, и обновляется ровно тот компонент, что на неё подписан. Минимум работы, максимум предсказуемости.
Главная ловушка при переходе на zoneless
// ❌ НЕ обновит экран в zoneless:
export class Bad {
x = 0; // обычное поле
ngOnInit() { setTimeout(() => this.x = 5, 1000); } // никто не разбудит CD
}
// ✅ Обновит:
export class Good {
x = signal(0); // сигнал
ngOnInit() { setTimeout(() => this.x.set(5), 1000); } // сигнал разбудит CD
}
Правило: в zoneless всё, что показывается в шаблоне и меняется во времени, должно
быть сигналом (или обновляться через AsyncPipe / markForCheck).
Короткий итог
- zone.js vs zoneless = «как часто и от чего вообще запускается проверка».
- Default vs OnPush = «проверять ли этот конкретный компонент, когда дошли до него».
- Самый дорогой режим — zone.js + Default. Самый чистый и дешёвый — zoneless + OnPush + signals (дефолт идеологии v21).
Дальше: а из чего вообще «сделан» компонент в памяти? → 06 — view, LView и TView.