The Default Change Detection Strategy

Angular developers are likely familiar with Zone.js. This library is impressive in what it accomplishes, but it also carries a certain amount of "magic" that can feel superfluous. Its sole function is to patch browser events so that any state changes within an application can be detected, triggering a view re-render automatically.

With Zone.js, the responsibility of deciding when to refresh the view is abstracted away. While this system is quite effective, it demands a significant amount of JavaScript to be loaded before the application can even initialize. Moreover, Zone.js doesn't pinpoint the source of a change; it simply re-evaluates the entire component tree whenever an event fires, leading to a potentially costly dirty-checking process.

Imagine clicking a button or modifying a property in a single component. Zone.js registers the interaction and initiates a full sweep of the component tree, descending from the root component through every nested child. Even though this sweep is highly optimized and executes quickly, it wastes effort recalculating parts of the application that are unaffected. The system only knows that an event occurred somewhere, not where the view might actually require an update.

A property is updated inside Component X

The entire component tree gets refreshed

The OnPush Strategy

To mitigate the overhead of the default strategy, Angular offers the ChangeDetectionStrategy.OnPush. This approach restricts change detection to only the component branch that is directly affected by a mutation, tracing down to the specific component where the modification took place.

✅ This increases efficiency by eliminating unnecessary traversal of the component tree.

❌ However, adopting the OnPush strategy demands a deeper understanding of Angular's mechanics from the developer. You need to be deliberate about triggering changes, with tools like markForCheck, the async pipe, or ensuring immutable inputs are provided.

(It is important to note that a deep dive into the OnPush strategy is not the goal of this article.)

A property is updated inside Component X

The component tree branch down to the impacted component gets refreshed

Change Detection with Signals

The quest for a more granular change detection system is the driving force behind Angular's new reactivity model. To address this, the framework has introduced a core primitive known as the “Signal”.

A signal is a wrapper around a piece of data, whether it's an object, a string, or any other value. Crucially, when you update the value within this wrapper, the framework can be notified of the exact View that consumes it, enabling a highly targeted refresh of just that part of the UI.

As of the time of this writing (Angular v16), signals are still in their early stages, and their integration into change detection still depends on Zone.js. The principles discussed below are based on the Angular RFC.

If we abstract a View to mean a component's template, and we update a signal that is read within it, the resulting process looks like this:

signal gets updated, and component X listens to that signal

only component X is refreshed

When a signal is modified via its set, update, or mutate method, all templates that are actively listening to it are alerted. As a result, those Views, and only those Views, are scheduled for re-rendering.

  • It's key to note that a re-render is only triggered by signals that are directly read within a template. If a signal is only used in a function's logic, no UI update will be triggered by its change.
  • When a View is re-rendered, it is re-evaluated in its entirety. This means that all the bindings within that template—not just the one that changed—will be recomputed.

Why not update individual bindings?

This is a deliberate trade-off made by the Angular team. They chose to set the unit of granularity as the View. Tracking dependencies down to the individual binding level would require a more complex and memory-intensive dependency graph. A View, however, is a sufficiently compact unit of the UI to guarantee fast and efficient re-rendering. (Keep in mind that with Zone.js, the framework would re-render the entire application.)

Defining a View

In this context, a View is defined as a fragment of the DOM that is contained within an ng-template. Every structural directive, including ngIf and ngFor, instantiates a new View. So, when you use ngFor to generate a large table, each row is its own distinct View. This means that a signal change impacting only one row will cause just that specific row's View to re-render, leaving the others untouched.

Extending our previous example, let's imagine our component X has two child Views. The re-render process would be confined as follows:

signal is updated, and VIEW 1 is listening to that signal

only VIEW 1 is updated

👉 In future Angular versions, this entire mechanism will function with or without Zone.js once signal-based components become fully supported.


This concludes the discussion. I hope this provides a clearer picture of how signals are set to enhance the performance of future Angular applications. The signal API is already available to experiment with in Angular v16.

The future of Angular looks incredibly promising. 🚀

You can connect with me on Twitter or Github. Feel free to reach out if you have any questions.