This article is an excerpt from my course Change detection in Angular

There are times when you'll observe a change detection cycle firing without an obvious trigger. Identifying what caused it can be a challenge. Within Angular, change detection can be initiated by all sorts of browser events—user interactions, network responses, or timers. In a complex application, these events are so interwoven that isolating the specific trigger for a given cycle is rarely straightforward. This is precisely where the browser's built-in debugging tools become essential.

Chrome DevTools is an invaluable asset when you need to trace the sequence of operations that culminates in a change detection run. In this segment, I'll demonstrate how to leverage the callstack, logpoints, filtering, and local overrides features to identify the source of those unexpected change detection cycles.

We'll begin with a straightforward case. Picture a UI element rendered on the page. You notice that merely hovering over this element causes change detection to fire. This is a typical scenario when a third-party library has silently attached event listeners to the DOM elements it uses.

To replicate this situation, we can use the subsequent implementation:

@Component({
  selector: 'app-root',
  template: `
  <div (mouseover)="0">Just a plain div</div>
  <child-cmp></child-cmp>
 `,
})
export class AppComponent {}
 
@Component({
  selector: 'child-cmp',
  template: `
    <div>Changes detected: {{n}}</div>
    <button (click)="fetch()">Fetch</button>
  `,
})
export class ChildComponent {
  n = String(Date.now()).slice(-4);
 
  ngDoCheck() {
    this.n = String(Date.now()).slice(-4);
  }
}

Which appears like this:

Useful Chrome DevTools techniques when debugging change detection in Angular — figure 1

Let's imagine we have no prior knowledge of the mouseover listener that's causing the change detection. Our goal is to uncover it using the DevTools. Here's the process.

First, we need to halt the change detection process. We can do this by setting a breakpoint within the template function of the component that's being affected—in this case, ChildComponent.

Once the change detection is paused, the screen will look like this:

Useful Chrome DevTools techniques when debugging change detection in Angular — figure 2

Your next step is to locate the component definition that the compiler generated in the Sources panel. If a global search doesn't help you find it, you can simply log the component instance to the console:

@Component({
  selector: 'child-cmp',
 ...
})
export class ChildComponent {
  constructor() {
    console.log(this)
  }
}

Then, use the "show function definition" feature, as shown below, to jump to the class definition:

Useful Chrome DevTools techniques when debugging change detection in Angular — figure 3

The template function generated by Angular's compiler will be located directly below the component class:

Useful Chrome DevTools techniques when debugging change detection in Angular — figure 4

After placing a breakpoint in the template function, hover over the box. When the debugger pauses execution, we can look at the callstack. In this scenario, we observe that Angular is executing a full application-wide change detection that began from the tick function:

Useful Chrome DevTools techniques when debugging change detection in Angular — figure 5

Had this been a local change detection run, the callstack would reveal the specific function that triggered it, rather than showing the tick method. For example, it might resemble this:

Useful Chrome DevTools techniques when debugging change detection in Angular — figure 6

For a global change detection that starts with the tick function, we need to examine what occurred just before it was called. Specifically, we're interested in the event that AngularZone processes before executing its checkStable function and notifying Angular accordingly.

Given the callstack from our example, we should investigate the orange segment that appears prior to the tick method:

Useful Chrome DevTools techniques when debugging change detection in Angular — figure 7

Our focus is on the event that ultimately triggers the call to tick. We can glean important details about the task from within the onInvokeTask callback. This is the callback that Zone.js invokes once a task has finished. Let's switch the execution context to that function by clicking on it in the callstack and then inspect the event data like this:

Useful Chrome DevTools techniques when debugging change detection in Angular — figure 8

From this, it becomes quite evident that the mouseover event is what's causing the change detection.

Leveraging logpoints

In real-world applications, there might be dozens or even hundreds of events firing in quick succession, each potentially triggering change detection. Stopping execution to examine each one individually isn't practical. In such cases, using logpoints proves to be a far more effective strategy.

Our task is to log every event that flows through the Zone.js mechanism. Let's attach a logpoint to the runTask method as follows:

Useful Chrome DevTools techniques when debugging change detection in Angular — figure 9

A zone task carries the following details:

  • source — the name of the API that requested the task to be scheduled
  • target — the event target, such as DOM elements for UI events
  • eventName — the native event name, like click or mouseover

Let's start by creating a logpoint that captures both the eventName and target properties:

Useful Chrome DevTools techniques when debugging change detection in Angular — figure 10

When we hover over the box on the page, this is what appears in the console:

Useful Chrome DevTools techniques when debugging change detection in Angular — figure 11

The logpoint clearly indicates that the event is mouseover and its target is the div element, which is our UI box.

We can also log the invocation of the tick method. This provides a clear indicator of when Angular is executing change detection:

Useful Chrome DevTools techniques when debugging change detection in Angular — figure 12

Now, upon hovering over the box, the console demonstrates the causal chain, showing how the mouseover event directly leads to a change detection cycle:

Useful Chrome DevTools techniques when debugging change detection in Angular — figure 13

However, change detection isn't always caused by immediate events. Sometimes it's triggered by a macrotask that has a noticeable duration between its initiation and completion, such as a network request or a timer. To diagnose these cases, we must also track the task's scheduling phase.

Tracing the task source

Let's modify our UI slightly. We'll add a button that initiates a network request:

Useful Chrome DevTools techniques when debugging change detection in Angular — figure 14

The corresponding implementation is:

@Component({
  selector: 'child-cmp',
 template: `
    <div>Records count: {{recordsCount}}</div>
    <button (click)="getTodos()">Fetch</button>
  `,
})
export class ChildComponent {
  getTodos() {
  fetch('https://jsonplaceholder.typicode.com/todos')
     .then(r => r.json())
     .then(c => this.recordsCount = c.length);
  }
}

In this setup, zone.js will manage two distinct tasks—the network call and several promises resolved via then. Our attention turns to the scheduling phase, which will lead us to the origin of the change detection run.

Task scheduling occurs inside the scheduleTask method. We'll add a logpoint there to output the source and type properties of the event:

Useful Chrome DevTools techniques when debugging change detection in Angular — figure 15

We'll also retain our logpoints in the tick and runTask methods:

Useful Chrome DevTools techniques when debugging change detection in Angular — figure 16

After clicking the button with these logpoints active, the console will display the following event sequence:

Useful Chrome DevTools techniques when debugging change detection in Angular — figure 17

We can observe the click handler executing and a number of promises being scheduled. However, there's no macrotask scheduled for the fetch API call. This is because zone.js does not schedule a task for the network request itself; it simply calls the browser API right away. The native API call is then wrapped in a promise, resulting in our three promises—two from the application code and one that wraps the fetch macrotask for zone.js.

While seeing that promises are scheduled is helpful, it doesn't point to the specific root cause of the promise microtask. To determine that, we'll set a breakpoint in the scheduleMicroTask function and inspect the callstack:

Useful Chrome DevTools techniques when debugging change detection in Angular — figure 18

This view allows us to see that the promise originates from the click event handler.

Using console.trace

Another technique involves using the console.trace expression within our logpoints:

Useful Chrome DevTools techniques when debugging change detection in Angular — figure 19

After clicking the button, the output looks like this:

Useful Chrome DevTools techniques when debugging change detection in Angular — figure 20

Once more, we can see that the promise was created in the click event handler.

But when there's a lot of console output, having traces expanded by default is inconvenient. To ensure they are collapsed upon logging, we can utilize the groupCollapsed API:

console.groupCollapsed(`schedule: ${task.type}, ${task.source}, ${task.type}`),
 console.trace(),
 console.groupEnd()

This code needs to execute when the script flow enters the scheduleTask function. Since logpoints can't handle this, we'll use a conditional breakpoint instead. Within the conditional breakpoint, we'll run statements to log the needed data. The key is that the last expression returns undefined, making the breakpoint condition evaluate to false, which stops the debugger from pausing execution.

The setup looks like this:

Useful Chrome DevTools techniques when debugging change detection in Angular — figure 21

Upon running it, we see log groups whose labels begin with the word "schedule":

Useful Chrome DevTools techniques when debugging change detection in Angular — figure 22

Expanding a group reveals the complete callstack:

Useful Chrome DevTools techniques when debugging change detection in Angular — figure 23

As we scroll through this callstack, it becomes apparent that the promise came from the click event handler.

Applying local overrides

Instead of relying on conditional breakpoints to run our expressions, we can use the local override API. With this, we can insert these expressions directly into the source code. Here's how:

Useful Chrome DevTools techniques when debugging change detection in Angular — figure 24

This source override feature proves to be exceptionally useful in many debugging contexts.


If you'd like to delve deeper into topics like this, check out the course:

Useful Chrome DevTools techniques when debugging change detection in Angular — figure 25