CONTACT
arrow_left Back to blog

The Progressive Journey of Change Detection in Angular

Introduction

In this article, we'll delve into the evolution of Angular's change detection strategies, exploring how they've transformed our applications to be more responsive and effective. Angular has made significant strides from the global top-down Default approach to the widely embraced OnPush strategy. However, the introduction of Signals-based methodology, as outlined in the Angular documentation promises even more significant efficiency gains in the change detection.

We'll explore the general inner workings of these strategies and analyze the performance gaps between them. Furthermore, we'll speculate on the direction of Angular's change detection mechanisms. So, let's get started!

Early Stage

Change detection kicks off with any asynchronous browser events, and Angular relies on the NgZone module to observe these interactions. When an async event occurs, Angular triggers the change detection process, traversing the view tree from top to bottom. However, there's a contrast in how components are marked for update across various strategies

Old-School Change detection

Back in the early days, Angular's default change detection was like a noisy neighbor. It kept an eye on every component, binding, and dependency whenever any event happened, leading to updates throughout the entire application. This method works, but it often leads to performance issues in bigger apps because it tends to perform checks even when everything has stayed the same.

Default Change Detection in Action

With the default change detection strategy, Angular keeps an eye on all components unless the view is explicitly detached:

@Component({
  selector: 'detached',
  template: `Detached Component`
})
export class DetachedComponent {
  constructor(private cdr: ChangeDetectorRef) {
    cdr.detach();
  }
}

This strategy relies on the CheckAlways flag:

let shouldRefreshView: boolean = !!(
  mode === ChangeDetectionMode.Global && flags & LViewFlags.CheckAlways
);

which ensures that Angular performs change detection on every view and executes the template function regardless of whether there are changes or not.

[Default Change Detection

Click on component "H," which creates a domino effect throughout the entire app, even when it's just a false alarm.

A Relaxed Approach to Change Detection

To speed things up, Angular brought in the OnPush change detection strategy. The OnPush option has been around just as long as the Default one. But to switch it on, developers needed to give their components a heads-up, letting them know they wanted to use it. OnPush is like a more discerning detective, only springing into action when a component's inputs change or events occur within that component. This optimized approach relied on immutable states and reduced unnecessary checks, making our apps more straightforward and faster. So, how does it work?

Unpacking the OnPush Change Detection Strategy

With the OnPush strategy, change detection takes a back seat unless a component is flagged as dirty:

shouldRefreshView ||= !!(
  flags & LViewFlags.Dirty &&
  mode === ChangeDetectionMode.Global &&
  !isInCheckNoChangesPass
);

To mark that a component as dirty, we can change an input property, emit an event, or manually invoke the markForCheck() method, which marks the current view and all its folks up the family tree, all the way to the root view, for a thorough check.

@Component({
  selector: 'dirty',
  template: `Dirty Component`,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DirtyComponent {
  constructor(private cdr: ChangeDetectorRef) {
    cdr.markForCheck();
  }
}

Angular then takes a tour of each dirty flagged view, executing the template function associated with its respective component.

OnPush Change Detection

With OnPush, a click in the component "H" triggers an event through clicked component itself and its parents.

Signals Era

From Angular v16 onwards, Angular has been moving towards adopting Signals, altering front-end state management. Unfortunately, as of version 17 , Signal-based Components remain under development, delaying the full implementation of Signal-based change detection. In the meantime, Angular allowed us to utilize the power of Signals and OnPush strategy to mark individual components as dirty. This Local Change Detection ensures no more unnecessary checks for parent or child components designated as OnPush.

Additionally, Angular's recent advancements in event detection include implementing signal inputs, where input value updates occur before the change detection cycle. This improvement ensures that change detection does not execute if the template doesn't use the input. Moreover, changing the value bound to an input no longer triggers a refresh of the parent view, as with the OnPush strategy.

Streamlining Change Detection with Signals

When we change the signal within a component's template using methods like set() or update() , we kick off a chain reaction. This signal triggers a notification to the reactive live consumer within the view, marking it as dirty through the markAncestorsForTraversal(). But it doesn't stop there – it also marks all ancestors, up to the root, with the HasChildViewsToRefresh flag. If the view is marked as dirty, the template function is called, and the current value is returned.

shouldRefreshView ||= !!(
  consumer?.dirty && consumerPollProducersForChange(consumer)
);

However, if the view is marked with the HasChildViewsToRefresh flag, there's no need to bother with the template function. It just takes a shortcut, skipping the template function and diving right into checking the children. This clever move helps Angular avoid unnecessary tasks, operations, calculations, and comparisons, ensuring everything runs smoothly and efficiently.

if (shouldRefreshView) {
  refreshView(tView, lView, tView.template, lView[CONTEXT]);
} else if (flags & LViewFlags.HasChildViewsToRefresh) {
  detectChangesInEmbeddedViews(lView, ChangeDetectionMode.Targeted);
  const components = tView.components;
  if (components !== null) {
    detectChangesInChildComponents(lView, components,
      ChangeDetectionMode.Targeted);
  }
}

The future

Looking ahead, Angular seems set to embrace a Signals-based Change Detection system fully. It's like getting a superpower to track changes with laser precision, eliminating the need for NgZone to manage execution context completely. By going zone-less, developers will have better, fine-grained control over change detection, enabling a more efficient and focused approach and optimizing performance even further by running Change Detection only when necessary. This approach will fundamentally change how Angular tracks and handles changes, allowing developers to pinpoint precisely where and when updates should occur.

However, transitioning to this new paradigm requires developers to understand signals better to harness their full potential. Despite the learning curve, the performance and scalability gains make it a worthwhile investment for projects aiming for optimal performance.

As Angular continues to evolve, developers must stay updated with these advancements in Change detection strategies.

The journey from the default strategy through OnPush and towards Signals-based Change Detection highlights Angular's commitment to enhancing performance and scalability. By adopting these advanced strategies, developers are empowered to build functional and exceptionally performant Angular applications.

Signal-based Change Detection

In the world of Local Change Detection, a change in component "F" gets the attention it deserves without redundantly checking parent or component children.

Conclusions

In conclusion, the evolution of change detection in Angular reflects a constant effort to refine and optimize performance. Embracing these advancements empowers developers to create Angular applications that are not just functional but also highly performant, ensuring a seamless user experience across a wide range of applications. Given the progress that has been made in this regard, I look forward with optimism to further releases of the Angular framework.



The principle of change detection can be viewed in this GitHub application.