
· 7 min read · Angular, Change detection, Performance
In-Depth Look at Angular's Change Detection, Mode Switching and DOM Traversal
How Angular marks components as dirty, and how it switches between Global and Targeted mode as it walks the component tree.
Contents
Overview
In this document, we will build upon our previous discussion of Angular's change detection system, which helps synchronize the UI with the app's internal state. By referring to the concepts outlined in the last documentation, we can gain a complete understanding of how Angular detects changes and updates the DOM.
Recap of Previous Documentation
In the previous document, we covered:
- Triggers of Change Detection: The role of Zone.js in automatically detecting changes through asynchronous events and how developers can manually control detection using
markForCheck(), or by updating asignal()with.set()or.update(). - Change Detection Strategies: We looked at the two main strategies, the Default Strategy and the OnPush Strategy.
- DOM Update Mechanism: We discussed how Angular traverses the component tree during change detection, ensuring the UI is always in sync with the data.
Expanding on Change Detection Algorithms
In this documentation, we will dive deeper into how Angular marks a component as dirty and how it uses Global and Targeted change detection algorithms to optimize the process of updating the DOM.
Marking a Component as Dirty
Angular identifies when part of the UI needs to be synchronized with the DOM by marking the component as dirty. There are three ways Angular determines a component's dirtiness:
- Marked for Check: When a component calls
markForCheck()(the same happens when one of its inputs changes or an event handler in its template runs), Angular marks that component and every ancestor up to the root as dirty. Nothing is checked at that moment; only the path is marked. On the next pass Angular refreshes that path, and the component's own children follow the normal rules: Default children are checked, OnPush children only if they are dirty themselves. - Maybe Dirty via Signal: When a signal read in a component's template changes, Angular marks only that component's view for refresh. Its ancestors are not marked dirty, so they are not re-rendered. It is only "maybe" dirty because a
computed()can produce the same value as before; Angular confirms that a value really changed before re-rendering the view. - Contains Dirtiness: When a view is marked for refresh by a signal, each of its ancestors gets a flag that says "not dirty, but a descendant is". Angular walks through those ancestors to reach the dirty view without re-rendering them.
By doing this, Angular now knows the application's dirtiness state and what operations need to happen at the application level (e.g., effects, rendering, and after-render hooks). This allows for fine-grained control over what needs to be updated in the UI.
Here's a simplified version of how Angular performs the change detection check at the top level:
while (dirty) {
if (dirty & Dirty.RootEffects) runRootEffects();
if (dirty & Dirty.Views) checkViews();
// If there are still dirty views, loop back
if (dirty & Dirty.Views) continue;
if (dirty & Dirty.RenderHooks) runRenderHooks();
}
This loop ensures that change detection is performed as long as there are dirty components or operations. The traversal occurs in cycles, ensuring no part of the tree is missed, and it handles cases where lifecycle hooks or signals might mark additional components as dirty. The loop is capped at 10 passes; if views are still dirty after that, Angular reports an infinite change detection error (NG0103) instead of hanging.
How Do We Check Views?
Angular checks views using two traversal modes: Global Mode and Targeted Mode. These are not the same thing as the Default and OnPush strategies; the strategy is a property of a component, the mode is the state of the traversal as it walks the tree.
- Global Mode: A view is refreshed if it uses the Default strategy, if it is marked dirty (for example by
markForCheck()), or if a signal it reads has changed. - Targeted Mode: A view is refreshed only if it was marked for refresh, for example because a signal it reads has changed. Using the Default strategy is not enough on its own.
In both modes, a view that is not refreshed but contains dirtiness is walked through without being re-rendered, and a view with neither is skipped together with its whole subtree.
These two modes allow Angular to prune parts of the component tree that do not require change detection, improving performance by skipping checks on components that cannot have updates.
When updates come from signals, change detection scales with the number of views that changed, not the size of the application.
Global Mode vs. Targeted Mode (Mode Switching)
Angular optimizes change detection by switching between Global Mode and Targeted Mode as needed during the traversal of the component tree. This ensures that only relevant components are checked, and the application remains performant, especially in large apps.
A change detection pass in an app that uses Zone.js starts at the root in Global Mode. In a zoneless app it starts in Targeted Mode; markForCheck() and template events still work there because they flag every view on the path to the root for refresh, not just as dirty.
As Angular traverses from parent to child, it switches between these modes based on the conditions:
- Global to Targeted: When Angular reaches a view that it does not refresh (for example an OnPush component that is not dirty) but that contains dirtiness, it walks into that view's children in Targeted Mode. Meeting an OnPush child is not a switch by itself: an OnPush child with nothing dirty inside it is simply skipped.
- Targeted to Global: When a view is refreshed while in Targeted Mode (for example because a signal it reads changed), its children are checked in Global Mode. Default children are refreshed, dirty OnPush children are refreshed, and OnPush children that are not dirty are still skipped.
This mode switching mechanism allows Angular to minimize unnecessary checks and improve performance by limiting the number of components visited during change detection.
Mode Switching in Practice: Examples
Example 1: Global Mode to Targeted Mode
Consider an application where you have a parent component and a child component. The parent component is set to use the Default Change Detection Strategy (which is Global Mode), and the child component is set to use the OnPush Change Detection Strategy (which is Targeted Mode).
Scenario:
- The parent component is triggered by an event (e.g., a button click or a network response) and gets marked as dirty.
- Because the parent component is using the Global Mode, Angular will check the parent and its children during the change detection cycle.
- Since the child component uses the OnPush Strategy, Angular will skip checking the child unless:
- The child's inputs have changed.
- The child component is marked for check using
markForCheck(). - A signal read in the child's own template has changed.
Reaching the OnPush child does not switch the mode. Angular is still in Global Mode and simply decides whether to refresh the child. The switch to Targeted Mode happens only when the child is skipped but contains dirtiness, for example when a grandchild reads a signal that changed. Angular then passes through the child without re-rendering it and refreshes only the grandchild.
Flow:
- The parent is dirty and checked in Global Mode.
- The child is refreshed if one of the conditions above is true. Otherwise it is skipped with its whole subtree.
- If the child is skipped but a grandchild was marked by a signal, Angular walks through the child in Targeted Mode and refreshes only the grandchild.
Example 2: Targeted Mode to Global Mode
Let's consider another example where we have an OnPush parent component whose template reads a signal. It has two children: one using the Default Strategy and one using OnPush. Everything above the parent is OnPush and not dirty.
Scenario:
- The signal changes. Angular marks the parent's view for refresh and flags its ancestors as containing dirtiness. Nothing is marked dirty.
- Angular starts at the root. The root is not dirty, but it contains dirtiness, so Angular walks down in Targeted Mode without re-rendering anything on the way.
- Angular reaches the parent. It was marked for refresh, so it is refreshed even though it is OnPush and in Targeted Mode.
- Because the parent was refreshed, its children are checked in Global Mode. The Default child is refreshed even though nothing in it changed. The OnPush child is skipped because it is not dirty.
Flow:
- The ancestors are walked through in Targeted Mode, not re-rendered.
- The parent is refreshed because its signal changed.
- The Default child is checked in Global Mode; the clean OnPush child is skipped.
Conclusion
Angular's change detection system uses a combination of Global Mode and Targeted Mode to optimize the performance of UI updates. The key to this optimization lies in the mode-switching mechanism, which ensures that only the necessary components are checked, thus minimizing the computational overhead during change detection. The flexibility to switch between these modes based on the state of each view (dirty, marked for refresh by a signal, or containing dirtiness) allows Angular to handle large and complex applications efficiently.
By understanding how dirty components are tracked and how mode switching occurs, developers can leverage Angular's change detection system to build performant, scalable applications that are both responsive and efficient.