skip to content

In Angular's provideZoneChangeDetection(), what do the eventCoalescing and runCoalescing options change, and what is the trade-off of enabling them?

level: middleimportance: nice to knowfreq 30%

answer

  1. a click bubbling through two listeners
  2. one tick instead of several
  3. animation frame or timeout
  4. the check becomes asynchronous

basics

~20 s

eventCoalescing merges the ticks from several DOM event handlers into one change detection; runCoalescing does the same for repeated NgZone.run() calls. Both defer that tick to the next animation frame or timeout, so views update slightly later.

solid answer

~40 s

Without coalescing, a click on a `<button (click)>` inside a `<div (click)>` runs two handlers, each in the Angular zone, and each exit can trigger `onMicrotaskEmpty` and a full `ApplicationRef.tick()`. `provideZoneChangeDetection({ eventCoalescing: true })` delays stabilization after event tasks and schedules one check with a race between `requestAnimationFrame` and `setTimeout`, so the bubbling handlers produce a single tick. `runCoalescing: true` applies the same delay to `NgZone.run()` calls, so a loop of ten `run()` calls causes one tick. Both default to `false`. The trade-off is timing: change detection no longer runs synchronously at the end of the handler, so code or tests that read the DOM straight after dispatching an event may see the old view. These options exist only for zone-based apps; zoneless scheduling already batches notifications.

code

ts · 9 lines
ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideZoneChangeDetection } from '@angular/core';
import { App } from './app/app';

bootstrapApplication(App, {
  providers: [
    provideZoneChangeDetection({ eventCoalescing: true, runCoalescing: true }),
  ],
});

go deeper

for a junior

Know that a bubbling click can trigger more than one change detection in a zone-based app and that eventCoalescing reduces it to one.

for a middle

Explain what each option coalesces, that both default to false, and that the tick is deferred to an animation frame or timeout.

for a senior

Weigh the fewer-ticks gain against asynchronous DOM updates, and know which tests and directives break when coalescing is turned on.

for a principal

Position coalescing as a cheap interim step in a zone-based app and compare its payoff with investing in a zoneless migration.

## The duplicate-tick problem In a zone-based Angular app, every patched callback that runs in the Angular zone can end with `onMicrotaskEmpty` and an `ApplicationRef.tick()`. DOM events make this visible: ```html <div (click)="selectRow()"> <button (click)="toggleTimer()">start</button> </div> ``` A click on the button bubbles, so two listeners run as two separate zone tasks. After the first, the zone leaves with no microtasks and Angular ticks; after the second, it ticks again. The first tick did work the second one immediately repeats. The same happens with a countdown whose buttons sit inside clickable cards, or with any code that calls `NgZone.run()` in a loop. ## What the options do `provideZoneChangeDetection()` accepts an `NgZoneOptions` object with two flags, both `false` by default: | Option | Coalesces | Typical source of duplicates | |---|---|---| | `eventCoalescing` | ticks after **event tasks** (DOM listeners) | bubbling or several listeners on one event | | `runCoalescing` | ticks after **`NgZone.run()`** invocations | loops or callbacks re-entering the zone repeatedly | ```ts import { bootstrapApplication } from '@angular/platform-browser'; import { provideZoneChangeDetection } from '@angular/core'; import { App } from './app/app'; bootstrapApplication(App, { providers: [provideZoneChangeDetection({ eventCoalescing: true })], }); ``` `bootstrapModule` has the equivalent `ngZoneEventCoalescing` and `ngZoneRunCoalescing` bootstrap options. ## How coalescing works When a coalesced task leaves the zone, `NgZone` does not stabilize immediately: 1. It marks a callback as scheduled and reports microtasks as still pending, so `onMicrotaskEmpty` is held back. 2. It schedules the stability check with a **race** between `requestAnimationFrame` and `setTimeout`, whichever fires first. 3. Further events in the same browser task see the callback already scheduled and add nothing. 4. When the scheduled callback runs, `NgZone` checks stability once, emits `onMicrotaskEmpty`, and Angular runs **one** tick. The result is one change-detection pass per frame-ish window instead of one per handler. ## The trade-off - **Asynchronous view updates.** Without coalescing, the DOM is updated by the time a zone task finishes. With it, the update waits for the scheduled callback. Code that dispatches an event and reads the DOM synchronously, whether in a test or in a directive, sees the old state. - **Tests need to wait.** Component tests should wait for stability (for example `await fixture.whenStable()`) rather than assert immediately after triggering an event. - **Gains depend on the app.** The benefit is proportional to how many duplicate ticks happen and how expensive a tick is. In an app whose ticks are cheap, the difference is small. ## Relation to zoneless These options exist to soften a zone.js cost: one tick per patched task. The zoneless scheduler, the default since v21, already batches notifications (signal writes, `markForCheck()`, template listeners) into a single scheduled tick, so there is nothing to configure there. In a legacy zone-based app, `eventCoalescing: true` is a low-risk improvement worth trying before a zoneless migration; `runCoalescing` matters mainly where code re-enters the zone in loops. ## Measuring whether it helps Before and after enabling the option, measure rather than assume: 1. Record an interaction (for example clicking the countdown's start button inside a clickable card) in the browser's performance panel or Angular DevTools' profiler. 2. Count the change-detection cycles attributed to that interaction. 3. Compare the total scripting time of the interaction, not just the cycle count. If the count drops from two or three cycles to one and each cycle is expensive, the option pays off. If each cycle is already cheap, the remaining cost is elsewhere, and the extra frame of latency may not be worth it. ## Quick recall - Both flags are `false` by default. - `eventCoalescing` targets DOM events; `runCoalescing` targets `NgZone.run()`. - Both defer the tick to a `requestAnimationFrame`/`setTimeout` race. - The cost is that change detection is no longer synchronous with the handler.

  • Why does a coalesced tick use a race between requestAnimationFrame and setTimeout?
    `requestAnimationFrame` aligns the check with the next paint, which is ideal for a visible tab, but browsers throttle or pause it in background tabs. Racing it with `setTimeout` guarantees the check still runs, whichever fires first.
  • Would you enable eventCoalescing in an existing large zone-based app without further work?
    Usually yes after running the test suite, because the change is small and the gain is fewer ticks per interaction. Failures typically come from tests or directives that read the DOM synchronously after an event; they need to wait for stability instead.

saying these in an interview costs you the question

  • eventCoalescing is enabled by default in provideZoneChangeDetection()
  • Coalescing skips change detection for some events entirely
  • runCoalescing merges ticks from all DOM events, not NgZone.run() calls
  • Zoneless apps need eventCoalescing to avoid duplicate ticks
  • With coalescing on, the DOM is still updated before the event handler returns