Angular 19 Zoneless: Продуктивність та Виявлення Змін Без Zone.js
Повний посібник з Angular zoneless change detection: видалення Zone.js, перехід на сигнали, налаштування provideZonelessChangeDetection, оптимізація SSR, міграція існуючих застосунків та підготовка до технічних співбесід з Angular.

Zoneless change detection в Angular являє собою найбільш значущу архітектурну зміну фреймворку з моменту впровадження standalone-компонентів. Повне видалення Zone.js з застосунку забезпечує зменшення розміру бандла приблизно на 33 КБ, скорочення зайвих циклів виявлення змін на 30-40% та чисті стек-трейси без специфічного шуму Zone.js.
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, який використовується виключно для тайміну анімації, все одно ініціює повний цикл виявлення змін. У великих застосунках з сотнями компонентів ці накладні витрати стають відчутними.
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 надає стабільну версію.
import { ApplicationConfig } from '@angular/core';
import { provideExperimentalZonelessChangeDetection } from '@angular/core';
export const appConfig: ApplicationConfig = {
providers: [
provideExperimentalZonelessChangeDetection(),
// No more Zone.js patching
]
};import { ApplicationConfig } from '@angular/core';
import { provideZonelessChangeDetection } from '@angular/core';
export const appConfig: ApplicationConfig = {
providers: [
provideZonelessChangeDetection(),
]
};Після зміни провайдера необхідно видалити zone.js з масиву polyfills у файлі angular.json для цілей build та test, а потім деінсталювати пакет:
# 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) представлення приєднується до дерева компонентів
import { Component, signal, computed } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
<div class="counter">
<button (click)="decrement()">-</button>
<span>{{ count() }}</span>
<button (click)="increment()">+</button>
<p>Double: {{ doubled() }}</p>
</div>
`
})
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 точно знає, які шаблони залежать від нього, і планує цілеспрямоване виявлення змін лише для цих представлень.
Застосування ChangeDetectionStrategy.OnPush є рекомендованим кроком до zoneless-сумісності, але не є суворо обов'язковим. Стратегія виявлення змін за замовчуванням продовжує працювати у zoneless-режимі. Водночас OnPush гарантує, що компоненти перерендерюються лише при зміні вхідних параметрів або виклику markForCheck(), що природно узгоджується з ментальною моделлю zoneless.
Міграція існуючих застосунків: типові помилки
Перехід від Zone.js до zoneless рідко зводиться до зміни одного рядка коду для діючих застосунків. Декілька патернів, що покладалися на неявну поведінку Zone.js, потребують явної обробки.
setTimeout та setInterval більше не ініціюють оновлення
У режимі Zone.js callback-функції setTimeout автоматично ініціювали виявлення змін. У zoneless-режимі цього не відбувається.
@Component({
selector: 'app-user-status',
template: `<span>{{ statusMessage }}</span>`
})
export class UserStatusComponent {
statusMessage = 'Loading...';
ngOnInit() {
setTimeout(() => {
// Zone.js would trigger CD here - zoneless does NOT
this.statusMessage = 'Ready';
}, 2000);
}
}import { Component, signal } from '@angular/core';
@Component({
selector: 'app-user-status',
template: `<span>{{ statusMessage() }}</span>`
})
export class UserStatusComponent {
statusMessage = signal('Loading...');
ngOnInit() {
setTimeout(() => {
// Signal update notifies Angular automatically
this.statusMessage.set('Ready');
}, 2000);
}
}Реактивні форми потребують явного сповіщення
Зміни стану форм через FormControl.setValue() або patchValue() не ініціюють автоматичне виявлення змін у zoneless-режимі. Два підходи вирішують цю проблему: підключення Observable-потоків форми до markForCheck() або відображення даних форми через сигнали.
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: `
<input [formControl]="searchControl" placeholder="Search..." />
<p>Results for: {{ searchTerm() }}</p>
`
})
export class SearchComponent {
searchControl = new FormControl('');
// Convert observable to signal for automatic template updates
searchTerm = toSignal(
this.searchControl.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged()
),
{ initialValue: '' }
);
}Готовий до співбесід з Angular?
Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.
Серверний рендеринг без Zone.js
SSR із zoneless Angular потребує особливої уваги. Zone.js раніше допомагав Angular визначати, коли застосунок досягнув "стабільного" стану, придатного для серіалізації. Без нього цю роль виконує сервіс PendingTasks.
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()) {
<div>{{ data()!.title }}</div>
} @else {
<div>Loading...</div>
}
`
})
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 на SharpSkill детально охоплює патерни виявлення змін, включаючи zoneless-сценарії, про які все частіше запитують на інтерв'ю. Для загального огляду варто ознайомитись з посібником топ-25 питань для Angular-співбесід. Модуль 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, що робить оновлення операцією перейменування - Сумісність сторонніх бібліотек залишається основним фактором ризику і потребує ретельного тестування перед розгортанням у продакшн
Починай практикувати!
Перевір свої знання з нашими симуляторами співбесід та технічними тестами.
Теги
Поділитися
Пов'язані статті

Angular 18 Signals: нові реактивні API та виявлення змін без Zone.js
Повний посібник з Angular 18 Signals: input(), model(), viewChild(), zoneless режим. Практичний гід з прикладами коду та міграцією компонентів.

Angular Standalone Components: Міграція та найкращі практики у 2026 році
Повний посібник з міграції Angular-додатків з NgModules на standalone-компоненти. Охоплює офіційний 3-кроковий процес CLI, lazy loading, маршрутизацію та найкращі практики для Angular 21.

Топ 25 Питань Співбесіди Angular: Повний Посібник до Успіху
25 найпоширеніших питань на співбесіді з Angular у 2026 році. Детальні відповіді, приклади коду та поради, щоб отримати позицію Angular-розробника.