Someone once asked me whether change detection in Angular traverses the component tree depth-first or breadth-first. In other words, does Angular visit the siblings of the currently checked component first (breadth-first), or does it descend into its children (depth-first)? I had never really pondered this, so I relied on my instincts and understanding of the internals, confidently answering that it was depth-first. To verify this, I built a component tree and added logging to the ngDoCheck hook:
@Component({
selector: 'r-comp',
template: `{{addRender()}}`
})
export class RComponent {
ngDoCheck() {
// holds all calls in order and is logged to console
calls.ngDoCheck.push('R');
}
To my surprise, the output indicated that some siblings were indeed processed before others, as illustrated in the diagram below:

The diagram shows Angular checking K and then V, followed by L and then C, and so forth. So, was my initial answer wrong, and is the algorithm actually breadth-first? Not quite. A closer look reveals it's not a true breadth-first traversal either. A standard breadth-first algorithm would visit all siblings at the same depth before moving deeper, but the diagram shows Angular checking L and C (siblings), and then, instead of proceeding to X and F, it descends to J and O. Moreover, the standard breadth-first algorithm is well-defined, but I couldn't find its implementation in the Angular source code. So, I ran another experiment, this time adding logging to a custom function that is invoked when change detection evaluates expressions:
@Component({
selector: 'r-comp',
template: `{{addRender()}}`
})
export class RComponent {
addRender() {
calls.render.push('R');
}
}
This time, the results were different:

The output now clearly shows a proper depth-first traversal. So what's behind this discrepancy? The explanation is quite straightforward, and I'll break it down.
Change detection operations
To understand this difference in behavior, we need to examine the sequence of operations that the change detection mechanism performs when it processes a component. If you've read my other articles on change detection, you'll likely recall that the core operations are:
- update the input properties of child components
- invoke the
NgOnChangesandNgDoChecklifecycle hooks on child components - update the DOM bindings of the current component
- run change detection for child components
One crucial detail stands out above: when Angular checks the current component, it triggers lifecycle hooks on its child components, but it updates the DOM for the current component itself. This distinction is extremely important. It is precisely why, when you place logging inside the NgDoCheck hook, the algorithm appears to be breadth-first. Let's trace this: while Angular is checking component K, it calls NgDoCheck on its direct children, L and C, which are siblings of each other. This results in the following sequence:

This looks like a breadth-first walk. However, remember that Angular is still in the middle of processing K itself. After finishing all operations for K, it does not move to the next sibling, V, as a breadth-first implementation would. Instead, it dives deep and begins checking L, the child of K. This is where the depth-first nature of the algorithm becomes evident. And, as we now understand, during the check of L, Angular will call ngDoCheck on J and O, which is exactly what the logs show:

So, in the end, my initial intuition was correct. The change detection core is a depth-first traversal, but it first invokes the ngDoCheck lifecycle hooks on a component's siblings as part of the parent's check phase. By the way, I have covered this exact logic in detail in the article If you think `ngDoCheck` means your component is being checked — read this article.
Stackblitz demo
You can explore the demo with logging logic in various places here.
