По праву основной фичей Angular 18 стала Zoneless Change Detection. Именно с ней так и хочется разобраться.
Одна из ключевых особенностей Angular — без преувеличения, мощнейший механизм обнаружения изменений, который отвечает, как ни странно, за обнаружение изменений и обновление вьюх.
Перед тем как мы перейдем к Zoneless Change Detection, вкратце пробежимся по концепции механизма CD (Change Detection) и тому, как он реализуется с помощью zone.js.
Механизм CD в Angular
Сам механизм отвечает за обнаружение изменений и обновление вьюх в приложении. Он гарантирует, что вьюха всегда синхронизируется с моделью. Так же стоит отметить, что CD работает путем обхода дерева и проверки изменений в каждом компоненте. При обнаружении изменений он обновляет вьюху и распространяет изменения на все дочерние компоненты.
zone.js и CD
Сама по себе zone.js — это просто библиотека, которая предоставляет механизм для обертывания кода и его выполнения в определенном контексте или (как ни странно) зоне.
Если упростить, зоны позволяют нам отслеживать вызовы асинхронщины.
Выходит так, что каждый раз, когда создается компонент:
Angular создает для него новую зону.
Зона отслеживает изменения, которые происходят в компоненте.
При необходимости запускает механизм обнаружения.
Пробежавшись по верхам, вернемся к главной теме. С выходом Angular 18 у нас появляется такая история как zoneless, что ��значает, что мы больше не будем использовать zone.js.
Звучит так, что придется все рефачить на сигналы…
Что ж, пробуем разбираться дальше.
Создадим новый проект с помощью следующей команды:
# ng-cli
ng new zoneless-app
# npx
npx ng new zoneless-app-npx
Идем в package.json убеждаемся, что версия 18 и идем дальше

Следующим шагом идем в angular.json и удаляем ‘zone.js’ из полифилов

Далее открываем ‘app.config.ts’ и видим, что у нас появился новый провайдер
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
// используем zone.js
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes)
]
};
Основная идея заключается в том, что теперь мы можем настроить нужен нам провайдер CD с зоной или без нее. Следующим шагом заменим его на provideExperimentalZonelessChangeDetection импортируя его из ‘@angular/core’
import { ApplicationConfig, provideExperimentalZonelessChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
// не используем zone.js
provideExperimentalZonelessChangeDetection(),
provideRouter(routes)
]
};
С этим провайдером zone.js не будет использоваться в приложении.
Тут стоит оговориться, что фича эксперементальная и использовать ее пока рано, а подробнее о ней можно почитать в доке.
Переходим к компонентам, чтобы уже посмотреть как это работает, создаем дочерний компонент со следующим содержимым:
import { Component, OnInit } from "@angular/core";
@Component({
standalone: true,
selector: "app-child",
template: <div>Current value: {{ currentValue }}</div>
})
export class ChildComponent implements OnInit {
public currentValue = 0;
ngOnInit(): void {
this.currentValue += 1;
}
}
Добавив ‘this.currentValue += 1;’ в ngOnInit мы видим, что все впорядке и в браузере currentValue будет отображаться как 1. Это происходит потому, что обновление синхронное и будет работать без зоны.

Но стоит добавить немного асинхронщин в виде setTimeout и все становится немного сложнее.
import { Component, OnInit } from "@angular/core";
@Component({
standalone: true,
selector: "app-child",
template: <div>Current value: {{ currentValue }}</div>
})
export class ChildComponent implements OnInit {
public currentValue = 0;
ngOnInit(): void {
setTimeout(() => {
this.currentValue += 1;
}, 1000)
}
}
В этом случае мы не увидим никаких изменений в браузере. Тут мы должны вручную запускать цикл обнаружения изменений, потому что автоматического обнаружения больше нет. Руками это можно сделать добавив changeDetectorRef и вызвать внутри таймаута markForCheck(); или detectChanges();
import { ChangeDetectorRef, Component, inject, OnInit } from "@angular/core";
@Component({
standalone: true,
selector: "app-child",
template: <div>Current value: {{ currentValue }}</div>
})
export class ChildComponent implements OnInit {
private changeDetectorRef = inject(ChangeDetectorRef);
public currentValue = 0;
ngOnInit(): void {
setTimeout(() => {
this.currentValue += 1;
this.changeDetectorRef.markForCheck();
}, 1000)
}
}
После этих действий мы видим, что значение снова изменилось т.к. мы отметили его “нуждающимся в изменениях”.
Из чего мы можем сделать вывод, что если мы планируем использовать zoneless, то самое время начать использовать onPush иначе придется дергать изменения руками, а еще это упростит обновление на новую версию в будущем.
Сигналы
Целиком и полностью уверен, что сигналы в этом случае будут работать из коробки, но все же убедимся в этом.
Меняем все на сигналы:
import { Component, OnInit, signal } from "@angular/core";
@Component({
standalone: true,
selector: "app-child",
template: <div>signal: {{ currentSignal() }}</div>
})
export class ChildComponent implements OnInit {
public currentSignal = signal(0);
ngOnInit(): void {
setTimeout(() => {
this.currentSignal.set(1);
}, 1000);
}
}
И в этом случае мы получаем вполне ожидаемое поведение, что подтверждает мои догадки.
Async pipe
И последним, но не по значению, проверим всеми любимый async pipe
Исправим компонент, чтобы его можно было использовать:
import { Component } from "@angular/core";
import { interval } from "rxjs";
import { AsyncPipe } from "@angular/common";
@Component({
standalone: true,
selector: "app-child",
imports: [
AsyncPipe
],
template: <div>Async pipe: {{ currentValue$ | async }}</div>
})
export class ChildComponent {
public currentValue$ = interval(1000);
}
И так же видим, что значение в браузере меняется каждую секунду, что так же подтверждает работоспособность rxjs.
Вывод
И так, если подытожить все вышесказанное, то основная проблема zoneless заключается в том, что автоматические обновления не будут запускаться как и раньше, а приложение без зоны по факту не принесет никакой оптимизации. Да размер бандла меньше, ведь мы не используем zone.js но основная идея остается той же: мы вносим изменения и говорим ангуляру о том что, нужно чекнуть обновления. Цикл так же пройдется по дереву, проверяя что нужно обновить.
А если мы хотим какой-то оптимизации, то как будто стоит смотреть в сторону сигналов, но с ними тоже нужно разбираться, а этим я вскоре займусь у себя в телеге.
