Angular
Change detection и zoneless
Долгие годы Angular угадывал, когда обновить экран. Библиотека zone.js перехватывала каждый setTimeout, каждый клик и каждый HTTP-ответ, и после любого из них Angular на всякий случай проверял всё дерево компонентов. Это работало, но цена была высока: лишние проверки и тяжёлый патч поверх браузерного API. С версии 20.2 zoneless-режим стабилен, а с 21 он стандарт для новых приложений. Теперь Angular узнаёт об изменениях не из перехвата браузера, а из сигналов. Меньше магии, меньше работы, предсказуемее производительность.
- Angular 20.2 (2025): zoneless объявлен стабильным, provideZonelessChangeDetection вышел из experimental
- Angular 21 (ноябрь 2025): zoneless - дефолт нового приложения через ng new, zone.js больше не ставится по умолчанию
- Дашборды с частыми обновлениями: OnPush и сигналы отсекают перерисовку поддеревьев, чьи данные не менялись
- Мобильный веб: удаление zone.js убирает десятки килобайт из бандла и патч поверх каждого браузерного API
- Команды на больших SPA годами вручную ставили OnPush ради производительности, теперь это движение к дефолту
Предварительные знания
- Сигналы: signal(), computed(), чтение значения вызовом signal()
- Понимание дерева компонентов и привязок в шаблоне
- Идея асинхронных операций: setTimeout, события DOM, HTTP-ответы
Зачем Angular понадобился zone.js
В AngularJS обнаружение изменений запускалось вручную через scope.apply или происходило внутри директив. При переписывании на Angular 2 команда искала способ узнавать об асинхронных событиях автоматически. Решением стал zone.js - порт идеи Dart-зон в JavaScript. Зона перехватывает (monkey-patch) все асинхронные API браузера: setTimeout, addEventListener, Promise, XHR. После завершения любой такой операции зона сообщает Angular, и тот прогоняет обнаружение изменений по дереву. Плюс: разработчик не думает, когда обновлять экран. Минус: Angular проверяет всё дерево после любого события, даже если ничего не поменялось, а сам патч добавляет вес и усложняет отладку стектрейсов. Сигналы (2023) дали точную альтернативу: фреймворк узнаёт об изменении ровно того значения, что изменилось, и zone.js становится не нужен.
Как работает обнаружение изменений
Обнаружение изменений (change detection, CD) - это процесс, в котором Angular сверяет выражения в шаблоне с текущим состоянием компонента и обновляет DOM там, где значение разошлось. Каждому компоненту соответствует детектор изменений. Когда запускается цикл CD, Angular обходит дерево детекторов сверху вниз, вычисляет привязки и применяет разницу к реальному DOM. Сам по себе обход быстрый, проблема в том, как часто и насколько широко он запускается.
Главный вопрос: что вообще запускает цикл. В классической модели с zone.js триггером служило любое асинхронное событие, перехваченное зоной: клик, таймер, ответ сети. После каждого такого события Angular по умолчанию проверял всё дерево компонентов, потому что не знал, где именно поменялись данные. Это надёжно, но расточительно: большая часть проверок ничего не находит.
Цикл CD по умолчанию однонаправленный сверху вниз. В dev-режиме Angular делает второй проход и кидает ExpressionChangedAfterItHasBeenCheckedError, если значение поменялось между первым и вторым проходом. Это защита от привязок, которые мутируют состояние во время рендера.
Что в классической модели с zone.js служило триггером цикла обнаружения изменений?
Стратегия OnPush
OnPush - это договор: компонент обещает, что его вид зависит только от входов (input), событий внутри него и прочитанных сигналов. В обмен Angular пропускает проверку этого компонента и его поддерева, пока не случится одно из разрешённых условий. Так широкий обход всего дерева превращается в точечную проверку только тех ветвей, где реально что-то поменялось.
- Один из input изменился по ссылке (новая ссылка на объект, а не мутация старого)
- Внутри компонента или его поддерева произошло событие из шаблона (например, click)
- Изменился сигнал, прочитанный в шаблоне этого компонента
- Сработала AsyncPipe или был вызван markForCheck вручную
С OnPush мутация объекта на месте не вызовет перерисовку: input меняется только при новой ссылке. user.name = 'новое' не сработает, нужно передать новый объект. Сигналы снимают этот подвох: signal.set всегда уведомляет детектор, поэтому связка OnPush + сигналы избавляет от ручного markForCheck.
- Default — Компонент проверяется в каждом цикле CD независимо от того, поменялись ли его данные. Просто, но расточительно на больших деревьях.
- OnPush — Компонент проверяется только при смене input по ссылке, событии в поддереве или изменении прочитанного сигнала. Меньше работы, требует иммутабельности или сигналов.
Компонент с OnPush принимает объект user через input. Код делает user.name = 'Анна' (мутация того же объекта). Перерисуется ли компонент?
Zoneless: сигналы как драйвер CD
Zoneless-режим убирает zone.js целиком. Триггером цикла CD становятся явные источники: изменение сигнала, прочитанного в шаблоне, событие из шаблона, завершение асинхронной операции фреймворка (например, отработка AsyncPipe или resource). Angular больше не патчит браузерные API и не проверяет дерево после каждого случайного setTimeout. С версии 20.2 режим стабилен, с 21 - дефолт нового приложения.
В zoneless сигнал - основной способ сообщить о новых данных. Когда signal.set меняет значение, Angular помечает грязными ровно те компоненты, что прочитали этот сигнал в шаблоне, и планирует их перерисовку. Никакого широкого обхода: уведомление идёт по точному графу зависимостей сигнала. Поэтому в zoneless-приложениях состояние держат в сигналах, а не в обычных полях.
Дорожная карта: zoneless стабилен с 20.2 и дефолт с 21, а с v22 OnPush - стратегия по умолчанию для новых компонентов. Направление одно: меньше неявных полных проверок дерева, больше точечных уведомлений от сигналов. Старые приложения на zone.js продолжают работать - режим включается опционально.
В zoneless-приложении что заставит компонент перерисоваться после смены данных?
Связь с другими темами
Урок про то, как Angular решает, что перерисовать. Дальше это связано с источниками данных:
- Сигналы — В zoneless именно сигналы помечают компонент грязным и запускают перерисовку
- Resource API и httpResource — Асинхронные данные заводятся в сигналы, и zoneless CD реагирует на них без подписок
Итог
- Обнаружение изменений - это сверка привязок шаблона с данными и обновление DOM там, где есть расхождение
- Исторически триггер давал zone.js, перехватывая все асинхронные API и заставляя Angular проверять всё дерево после каждого события
- OnPush сужает проверку: компонент перепроверяется только при смене входов по ссылке, событии внутри него или изменении прочитанного сигнала
- Zoneless (стабилен с 20.2, дефолт с 21) убирает zone.js: триггером становятся сигналы, события шаблона и завершение async-операций фреймворка
- Сигналы дают точечность: помечается грязным ровно тот компонент, что прочитал изменившийся сигнал; в v22 OnPush стал стратегией по умолчанию для новых компонентов
Связанные уроки
- ng-11-signals-intro — Сигналы - предпосылка: именно они в zoneless-режиме сообщают Angular, что нужна перерисовка
- ng-27-resource-httpresource — Resource API заводит асинхронные данные в сигналы, и zoneless CD реагирует на них напрямую