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 пропускает проверку этого компонента и его поддерева, пока не случится одно из разрешённых условий. Так широкий обход всего дерева превращается в точечную проверку только тех ветвей, где реально что-то поменялось.

  1. Один из input изменился по ссылке (новая ссылка на объект, а не мутация старого)
  2. Внутри компонента или его поддерева произошло событие из шаблона (например, click)
  3. Изменился сигнал, прочитанный в шаблоне этого компонента
  4. Сработала 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 реагирует на них напрямую
Change detection и zoneless

0

1

Войти