Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке ngzone в Angular?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**NgZone** — контекст виконання Angular на основі Zone.js, який автоматично запускає виявлення змін (change detection), коли async-задачі завершуються всередині зони. ```typescript this.ngZone.runOutsideAngular(() => { setInterval(() => this.counter++, 16); // CD не запускається }); this.ngZone.run(() => { this.data = freshData; }); // CD запускається один раз ``` **Головне:** Zone.js патчить браузерні API (setTimeout, Promise, події), щоб Angular знав про завершення задач. `runOutsideAngular()` для важких циклів без оновлення UI.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**NgZone** — це обгортка Angular над Zone.js, яка патчить браузерні async API і автоматично запускає виявлення змін (change detection), коли задачі завершуються всередині Angular-зони. ## Теорія ### TL;DR - NgZone схожий на датчик руху: будь-яка async-активність всередині нього (setTimeout, fetch) сигналізує Angular перевірити UI. - Всередині зони Angular запускає change detection автоматично. Зовні він ігнорує результати async-операцій. - `runOutsideAngular()` для важких циклів, які не чіпають UI. `run()` щоб повернути колбеки сторонніх бібліотек назад у зону. - Zone.js патчить 40+ браузерних API при завантаженні, підміняючи нативні виклики. - Angular 17+ підтримує zoneless-режим через signals як альтернативний підхід. ### Швидкий приклад ```typescript import { Component, NgZone, OnInit } from '@angular/core'; @Component({ selector: 'app-demo', template: `<p>{{count}}</p>` }) export class DemoComponent implements OnInit { count = 0; constructor(private ngZone: NgZone) {} ngOnInit() { // Всередині зони: Angular бачить зміну і оновлює шаблон setTimeout(() => this.count++, 1000); // Поза зоною: count зростає в пам'яті, шаблон показує 0 this.ngZone.runOutsideAngular(() => { setTimeout(() => this.count++, 1000); }); } } ``` Перший `setTimeout` перехоплений Zone.js, тому Angular знає що треба запустити change detection після його завершення. Другий виконується у форкнутій зоні, за якою Angular не стежить. ### Як це працює всередині Zone.js підміняє нативні браузерні API при завантаженні. Коли код викликає `window.setTimeout`, насправді виконується версія Zone.js: вона форкує дочірню зону, запускає колбек і сповіщає `NgZoneImpl` про завершення. Angular ставить у чергу `ApplicationRef.tick()` через `queueMicrotask`, який проходить деревом компонентів і перевіряє зміни. В dev-режимі Angular перевіряє все дерево. З компонентами `OnPush` перевіряються тільки позначені піддерева. Тригер однаковий в обох випадках: задача завершилась всередині зони. Колбеки WebSocket, нативний `requestAnimationFrame` і багато подій сторонніх бібліотек за замовчуванням виконуються поза Angular-зоною. Zone.js не патчить всі async-шляхи. Звідси і виникає більшість NgZone-багів. ### Коли використовувати - Важкі анімаційні цикли і `setInterval`-лічильники, які оновлюють UI тільки в кінці: `runOutsideAngular()` зупиняє Angular від запуску change detection 60 разів на секунду. - Колбеки WebSocket або сторонніх бібліотек, які мають оновлювати шаблон: загортай в `ngZone.run()`, щоб повернути їх у зону. - Server-side rendering з Angular Universal: перевіряй `NgZone.isInAngularZone()` перед DOM-мутаціями, щоб уникнути проблем із гідратацією. - Будь-який async, що оновлює `@Input` або `@Output` привʼязки: залишай всередині зони (поведінка за замовчуванням). ### Типові помилки **Помилка 1: Припущення, що колбеки WebSocket всередині зони** ```typescript // Неправильно: шаблон ніколи не оновиться const socket = new WebSocket('ws://api.example.com/data'); socket.onmessage = (event) => { this.data = event.data; // Zone.js не повністю патчить WebSocket }; // Правильно socket.onmessage = (event) => { this.ngZone.run(() => { this.data = event.data; }); }; ``` Перший раз це зʼявилось у реал-тайм дашборді: дані WebSocket приходили, масив ріс, але графік не оновлювався. Zone.js просто не стежив за цим колбеком. **Помилка 2: `runOutsideAngular()` для коду, що оновлює UI** ```typescript // Неправильно: лічильник замирає в шаблоні this.ngZone.runOutsideAngular(() => { setInterval(() => this.counter++, 1000); }); // Правильно: повертайся в зону при кожному оновленні this.ngZone.runOutsideAngular(() => { setInterval(() => { this.counter++; this.ngZone.run(() => {}); // Примусовий tick }, 1000); }); ``` **Помилка 3: Не виносити інтервали поза зону від самого початку** ```typescript // Неправильно: зона відстежує задачу весь час ngOnInit() { this.timer = setInterval(() => this.frameCount++, 16); // Всередині зони } ngOnDestroy() { clearInterval(this.timer); // Зона стежила весь час } // Правильно: зона ніколи не дізнається про цей інтервал ngOnInit() { this.ngZone.runOutsideAngular(() => { this.timer = setInterval(() => this.frameCount++, 16); }); } ngOnDestroy() { clearInterval(this.timer); } ``` **Помилка 4: Вкладені виклики `run()` в Angular 16+** Виклик `ngZone.run()` всередині вже активної зони створює вкладені зони і дублює виклики `tick()`. Спочатку перевіряй: ```typescript const update = () => { this.value = newValue; }; if (this.ngZone.isInAngularZone()) { update(); } else { this.ngZone.run(update); } ``` ### Де зустрічається в реальних проектах - NG Bootstrap: `NgbModal` використовує `runOutsideAngular()` для анімаційних фреймів, щоб досягти 60fps. - PrimeNG DataTable: обробники віртуального скролу виконуються поза зоною, щоб не запускати CD на кожен піксель прокрутки. - Angular Material CDK: події оверлеїв проходять через `NgZone.run()` при відкритті і закритті. - RxJS у сервісах: `tap(() => this.ngZone.run(() => this.update()))` коли observable починається поза зоною. - Angular Universal SSR: `isBrowser ? ngZone.run(...) : directDom(...)` для запобігання невідповідностей гідратації. `ChangeDetectorRef.detectChanges()` — компонентна альтернатива: швидша, бо не обходить все дерево. Використовуй `NgZone` коли тригер приходить із сервісу або cross-component async-джерела. ### Follow-up питання **Q:** Як Zone.js патчить `setTimeout` і `addEventListener`? **A:** `window.setTimeout` підміняється враппером, який викликає `currentZone.runGuarded(fn)`. `addEventListener` загортає обробник у проксі, що форкує дочірню зону при виклику. Обидва сповіщають `NgZoneImpl` після завершення задачі. **Q:** Яка вартість перебування всередині Angular-зони? **A:** Кожен виклик `ApplicationRef.tick()` коштує 10-50мс на великих застосунках через обхід дерева компонентів. Важкі цикли поза зоною уникають цих перевірок, але кожне оновлення UI потребує ручного `run()`. **Q:** В Angular 17+ із signals NgZone ще актуальний? **A:** Signals використовують планувальник замість zone-патчингу, тому zoneless-застосунки можуть відмовитись від Zone.js. Якщо є legacy-код або сторонні бібліотеки, NgZone залишається потрібним як міст між двома підходами. **Q:** В компоненті `OnPush` з `async` pipe NgZone впливає на виявлення змін? **A:** `async` pipe позначає компонент dirty через `ChangeDetectorRef.markForCheck()`, а не через зону. Якщо observable приходить поза зоною, `async` pipe сам по собі недостатній: потрібен `ngZone.run()`, щоб запланувати tick, який прочитає цей dirty-прапор. **Q:** (Senior) Як відлагодити заморожений UI після оновлення сторонньої бібліотеки? **A:** В консолі браузера виконай `ng.probe($0).injector.get(NgZone).isInAngularZone()` поки застосунок обробляє дані. Якщо повертає `false`, колбек бібліотеки виконується поза зоною. Загорни в `ngZone.run()` або перейди на версію з нативною інтеграцією Angular. ## Приклади ### WebSocket dashboard ```typescript import { Component, NgZone, OnDestroy } from '@angular/core'; @Component({ selector: 'app-dashboard', template: `<ul><li *ngFor="let d of data">{{d}}</li></ul>` }) export class DashboardComponent implements OnDestroy { data: number[] = []; private socket: WebSocket; constructor(private ngZone: NgZone) { this.socket = new WebSocket('ws://api.example.com/metrics'); // onmessage за замовчуванням виконується поза Angular-зоною this.socket.onmessage = (event) => { this.ngZone.run(() => { // Всередині зони: Angular бачить зміну і оновлює список this.data.push(+event.data); }); }; } ngOnDestroy() { this.socket.close(); } } // Результат: нові значення одразу зʼявляються в шаблоні ``` Без `ngZone.run()` масив зростає в памʼяті, але шаблон не перерендерюється. Це найпоширеніший NgZone-баг у production-дашбордах. ### Нескінченний скрол на великих списках ```typescript import { Component, NgZone } from '@angular/core'; @Component({ selector: 'app-list', template: ` <div *ngFor="let item of items">{{item}}</div> <p *ngIf="loading">Завантаження...</p> ` }) export class InfiniteListComponent { items: string[] = []; loading = false; constructor(private ngZone: NgZone) { window.addEventListener('scroll', () => { if (window.innerHeight + window.scrollY >= document.body.offsetHeight) { this.loadMore(); } }); } private async loadMore() { this.loading = true; const newItems = await fetchMore(); // Promise поза зоною // Повертаємось в зону тільки коли дані готові this.ngZone.run(() => { this.items.push(...newItems); this.loading = false; }); } } // Результат: плавний скрол на 10k+ елементах, CD лише при отриманні даних ``` Change detection спрацьовує один раз на пакет завантаження. Не на кожен піксель прокрутки. ### Progress bar із важкими обчисленнями ```typescript import { Component, NgZone } from '@angular/core'; @Component({ selector: 'app-progress', template: `<div [style.width.%]="progress"></div>` }) export class ProgressComponent { progress = 0; constructor(private ngZone: NgZone) {} startHeavyTask() { // 1000 анімаційних фреймів поза зоною, один фінальний рендер this.ngZone.runOutsideAngular(() => { let i = 0; const tick = () => { i++; this.progress = i / 10; // Оновлює тільки внутрішнє значення if (i < 1000) { requestAnimationFrame(tick); } else { // Повертаємось в зону для фінального рендеру this.ngZone.run(() => { this.progress = 100; }); } }; requestAnimationFrame(tick); }); } } // Результат: 0 виявлень змін під час 1000 фреймів, 1 фінальний рендер ```Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.