# Angular 19 Zoneless: Продуктивність та Виявлення Змін Без Zone.js
> Повний посібник з Angular zoneless change detection: видалення Zone.js, перехід на сигнали, налаштування provideZonelessChangeDetection, оптимізація SSR, міграція існуючих застосунків та підготовка до технічних співбесід з Angular.
- Published: 2026-04-27
- Updated: 2026-04-27
- Author: SharpSkill
- Tags: angular, zoneless, change-detection, performance, signals, zone-js
- Reading time: 10 min
---
Zoneless change detection в Angular являє собою найбільш значущу архітектурну зміну фреймворку з моменту впровадження standalone-компонентів. Повне видалення Zone.js з застосунку забезпечує зменшення розміру бандла приблизно на 33 КБ, скорочення зайвих циклів виявлення змін на 30-40% та чисті стек-трейси без специфічного шуму Zone.js.
> **Хронологія Zoneless**
>
> Angular 18 представив zoneless у статусі experimental. Angular 19 вдосконалив експериментальний API через `provideExperimentalZonelessChangeDetection()`. Angular 20 перевів його у стабільний статус як `provideZonelessChangeDetection()`. Angular 21 зробив zoneless режимом за замовчуванням для нових проєктів.
## Як працює виявлення змін через Zone.js під капотом
Перед розглядом zoneless-підходу варто детально розібрати механізм роботи Zone.js. Ця бібліотека перехоплює (monkey-patch) кожен асинхронний API браузера: `setTimeout`, `setInterval`, `Promise.then`, `addEventListener`, `XMLHttpRequest` та десятки інших. Щоразу, коли один з цих API завершує виконання, Zone.js повідомляє Angular, який запускає цикл виявлення змін по всьому дереву компонентів.
Такий підхід має фундаментальний недолік: Zone.js не має інформації про те, чи дійсно змінився стан застосунку. Виклик `setTimeout`, який використовується виключно для тайміну анімації, все одно ініціює повний цикл виявлення змін. У великих застосунках з сотнями компонентів ці накладні витрати стають відчутними.
```typescript
// app.config.ts - Traditional Zone.js setup (Angular 18-19)
import { ApplicationConfig } from '@angular/core';
import { provideZoneChangeDetection } from '@angular/core';
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
// Zone.js patches ~130+ browser APIs
// Every async callback triggers change detection
]
};
```
Об'єднання подій (event coalescing), впроваджене в Angular 14, частково зменшує накладні витрати шляхом групування кількох подій в один цикл виявлення змін. Проте фундаментальна проблема залишається: виявлення змін запускається значно частіше, ніж це необхідно.
## Увімкнення Zoneless Change Detection в Angular 19 та 20
Шлях міграції відрізняється залежно від версії Angular. Angular 19 використовує експериментальний API, тоді як Angular 20 надає стабільну версію.
```typescript
// app.config.ts - Angular 19 (experimental)
import { ApplicationConfig } from '@angular/core';
import { provideExperimentalZonelessChangeDetection } from '@angular/core';
export const appConfig: ApplicationConfig = {
providers: [
provideExperimentalZonelessChangeDetection(),
// No more Zone.js patching
]
};
```
```typescript
// app.config.ts - Angular 20+ (stable)
import { ApplicationConfig } from '@angular/core';
import { provideZonelessChangeDetection } from '@angular/core';
export const appConfig: ApplicationConfig = {
providers: [
provideZonelessChangeDetection(),
]
};
```
Після зміни провайдера необхідно видалити `zone.js` з масиву polyfills у файлі `angular.json` для цілей build та test, а потім деінсталювати пакет:
```bash
# Remove zone.js polyfill from angular.json build and test targets
# Then uninstall the package
npm uninstall zone.js
```
Зменшення розміру бандла відбувається миттєво: Zone.js займає приблизно 33 КБ у сирому вигляді (10 КБ у gzip) коду, що завантажується негайно.
## Що ініціює виявлення змін у Zoneless-режимі
Без перехоплення кожної асинхронної операції засобами Zone.js, Angular покладається на явні сповіщення. Фреймворк планує виявлення змін при виникненні будь-якої з наступних умов:
- Сигнал, прочитаний у шаблоні, оновлює своє значення
- Викликається `ChangeDetectorRef.markForCheck()` (автоматично через `AsyncPipe`)
- Вхідний параметр компонента змінюється через `ComponentRef.setInput()`
- Виконується callback обробника подій шаблону або хоста (click, input тощо)
- Раніше позначене як "брудне" (dirty) представлення приєднується до дерева компонентів
```typescript
// counter.component.ts - Signal-driven change detection
import { Component, signal, computed } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
{{ count() }}
Double: {{ doubled() }}
`
})
export class CounterComponent {
// Signal updates automatically notify the template
count = signal(0);
doubled = computed(() => this.count() * 2);
increment() {
this.count.update(v => v + 1);
// No markForCheck() needed - signal handles notification
}
decrement() {
this.count.update(v => v - 1);
}
}
```
Сигнали є природним доповненням zoneless-режиму. Коли значення сигналу змінюється, Angular точно знає, які шаблони залежать від нього, і планує цілеспрямоване виявлення змін лише для цих представлень.
> **OnPush рекомендовано, але не обов'язково**
>
> Застосування `ChangeDetectionStrategy.OnPush` є рекомендованим кроком до zoneless-сумісності, але не є суворо обов'язковим. Стратегія виявлення змін за замовчуванням продовжує працювати у zoneless-режимі. Водночас OnPush гарантує, що компоненти перерендерюються лише при зміні вхідних параметрів або виклику `markForCheck()`, що природно узгоджується з ментальною моделлю zoneless.
## Міграція існуючих застосунків: типові помилки
Перехід від Zone.js до zoneless рідко зводиться до зміни одного рядка коду для діючих застосунків. Декілька патернів, що покладалися на неявну поведінку Zone.js, потребують явної обробки.
### setTimeout та setInterval більше не ініціюють оновлення
У режимі Zone.js callback-функції `setTimeout` автоматично ініціювали виявлення змін. У zoneless-режимі цього не відбувається.
```typescript
// user-status.component.ts - Before: relies on Zone.js
@Component({
selector: 'app-user-status',
template: `{{ statusMessage }}`
})
export class UserStatusComponent {
statusMessage = 'Loading...';
ngOnInit() {
setTimeout(() => {
// Zone.js would trigger CD here - zoneless does NOT
this.statusMessage = 'Ready';
}, 2000);
}
}
```
```typescript
// user-status.component.ts - After: signal-based approach
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-user-status',
template: `{{ statusMessage() }}`
})
export class UserStatusComponent {
statusMessage = signal('Loading...');
ngOnInit() {
setTimeout(() => {
// Signal update notifies Angular automatically
this.statusMessage.set('Ready');
}, 2000);
}
}
```
### Реактивні форми потребують явного сповіщення
Зміни стану форм через `FormControl.setValue()` або `patchValue()` не ініціюють автоматичне виявлення змін у zoneless-режимі. Два підходи вирішують цю проблему: підключення Observable-потоків форми до `markForCheck()` або відображення даних форми через сигнали.
```typescript
// search.component.ts - Reactive forms with zoneless
import { Component, inject, signal } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { toSignal } from '@angular/core/rxjs-interop';
import { debounceTime, distinctUntilChanged } from 'rxjs';
@Component({
selector: 'app-search',
imports: [ReactiveFormsModule],
template: `
Results for: {{ searchTerm() }}
`
})
export class SearchComponent {
searchControl = new FormControl('');
// Convert observable to signal for automatic template updates
searchTerm = toSignal(
this.searchControl.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged()
),
{ initialValue: '' }
);
}
```
## Серверний рендеринг без Zone.js
SSR із zoneless Angular потребує особливої уваги. Zone.js раніше допомагав Angular визначати, коли застосунок досягнув "стабільного" стану, придатного для серіалізації. Без нього цю роль виконує сервіс `PendingTasks`.
```typescript
// data-loader.component.ts - SSR-compatible async loading
import { Component, inject, signal } from '@angular/core';
import { PendingTasks } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
@Component({
selector: 'app-data-loader',
template: `
@if (data()) {
{{ data()!.title }}
} @else {
Loading...
}
`
})
export class DataLoaderComponent {
private http = inject(HttpClient);
private pendingTasks = inject(PendingTasks);
data = signal<{ title: string } | null>(null);
ngOnInit() {
// PendingTasks.run() prevents SSR serialization until complete
this.pendingTasks.run(async () => {
const result = await firstValueFrom(
this.http.get<{ title: string }>('/api/data')
);
this.data.set(result);
});
}
}
```
Без використання `PendingTasks` Angular серіалізує сторінку до завершення завантаження асинхронних даних, відправляючи клієнту порожній контент.
## Бенчмарки продуктивності: Zone.js проти Zoneless
Покращення продуктивності від видалення Zone.js поділяються на три категорії:
| Метрика | Zone.js | Zoneless | Покращення |
|--------|---------|----------|-------------|
| Початковий розмір бандла | +33 КБ raw / +10 КБ gzip | 0 КБ накладних витрат | 100% зменшення |
| Цикли виявлення змін (типовий застосунок) | 150-300 на взаємодію | 5-15 на взаємодію | На 80-95% менше |
| Глибина стек-трейсу | 8-12 додаткових Zone-фреймів | Чисті нативні трейси | Миттєва ясність |
| Time to Interactive (TTI) | Базовий рівень | На 15-25% швидше | Усунено завантаження Zone |
Найбільш суттєве покращення спостерігається у застосунках з інтенсивною асинхронною активністю: HTTP-поллінг, WebSocket-з'єднання, таймери та складні обробники подій. Кожен з цих механізмів раніше ініціював непотрібні цикли виявлення змін, які zoneless-режим повністю усуває.
> **Сумісність бібліотек**
>
> Деякі сторонні бібліотеки досі залежать від Zone.js внутрішньо. Модальні діалоги, окремі обгортки Web Components та деякі бібліотеки анімацій можуть поводитися непередбачувано в zoneless-режимі. Необхідно ретельно тестувати інтеграції бібліотек перед розгортанням zoneless у продакшн.
## API NgZone, які залишаються після переходу
Поширена помилка: видалення Zone.js означає видалення усіх посилань на `NgZone`. Це невірно. `NgZone.run()` та `NgZone.runOutsideAngular()` залишаються сумісними із zoneless-застосунками і мають зберігатися у спільних бібліотеках. Їх видалення може спричинити регресію продуктивності у застосунках, які досі використовують Zone.js та споживають ці бібліотеки.
Проте три Observable-потоки `NgZone` необхідно усунути:
- `NgZone.onMicrotaskEmpty` — ніколи не спрацьовує у zoneless-режимі
- `NgZone.onUnstable` — ніколи не спрацьовує у zoneless-режимі
- `NgZone.onStable` — ніколи не спрацьовує у zoneless-режимі
Логіку, залежну від таймінгу та побудовану на цих Observable-потоках, слід замінити на `afterNextRender()` або `afterEveryRender()` з `@angular/core`.
## Від Angular 19 Experimental до Angular 21 за замовчуванням
Еволюція zoneless API має чітку траєкторію стабілізації:
- **Angular 18.1**: `provideExperimentalZonelessChangeDetection()` представлено як experimental
- **Angular 19**: Експериментальний API вдосконалено, розширене тестування екосистеми
- **Angular 20**: Перейменовано на `provideZonelessChangeDetection()`, переведено у стабільний статус
- **Angular 20.2**: API повністю стабільний, без очікуваних змін поведінки
- **Angular 21**: Zoneless стає режимом за замовчуванням для `ng new`, виклик провайдера не потрібен
Для команд на Angular 19 шлях оновлення є прямолінійним: оновлення до Angular 20, заміна `provideExperimentalZonelessChangeDetection` на `provideZonelessChangeDetection` та видалення polyfill `zone.js`. Підтримка Angular 19 завершується 19 травня 2026 року, що робить цю міграцію терміновою.
Підготовка до співбесід з Angular? Модуль [питань для співбесід з Angular](/technologies/angular/interview-questions/change-detection) на SharpSkill детально охоплює патерни виявлення змін, включаючи zoneless-сценарії, про які все частіше запитують на інтерв'ю. Для загального огляду варто ознайомитись з посібником [топ-25 питань для Angular-співбесід](/blog/angular/top-25-angular-interview-questions). Модуль [Angular Signals](/technologies/angular/interview-questions/angular-signals) також розкриває модель реактивності, яка робить zoneless можливим.
## Підсумки
- Видалення Zone.js усуває 33 КБ ваги бандла та прибирає шар monkey-patching, який перехоплював понад 130 API браузера
- Angular-сигнали забезпечують явну модель реактивності, яка робить zoneless практичним, автоматично сповіщаючи шаблони при зміні стану
- Міграція вимагає перетворення патернів `setTimeout`/`setInterval`, реактивних форм та логіки, залежної від таймінгу, на підходи на основі сигналів або `markForCheck()`
- SSR-застосунки мають впровадити `PendingTasks` для заміни механізму визначення стабільності Zone.js
- `NgZone.run()` та `NgZone.runOutsideAngular()` слід зберігати у спільних бібліотеках для зворотної сумісності
- Експериментальний API Angular 19 (`provideExperimentalZonelessChangeDetection`) безпосередньо відповідає стабільному `provideZonelessChangeDetection()` Angular 20, що робить оновлення операцією перейменування
- Сумісність сторонніх бібліотек залишається основним фактором ризику і потребує ретельного тестування перед розгортанням у продакшн
---
Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack.
HTML version of this page: https://sharpskill.dev/uk/blog/angular/angular-19-zoneless-change-detection-performance