DEV Community

Cover image for Angular 18 — Zoneless & Change Detection
Maksim Dolgih
Maksim Dolgih

Posted on • Originally published at Medium

Angular 18 — Zoneless & Change Detection

Clear examples of change detection on different application triggers. Click, Input + NgModel, AsyncPipe, Signal, Web API — setInterval

After years of waiting and practicing “how to tame the dragon” skills, the Angular team has finally released an update that allows you to test building applications without using ZoneJs

In this article I would like to show how change detection is performed across the entire component tree when using different triggers, namely:

  • Click event — button click event + handler

  • Input + NgModel — processing of user input with saving to a variable via two-way data binding

  • AsyncPipe — output value into a template from Observable

  • Signal — output value into a template from toSignal(obs$)

  • setInterval — Web API

These triggers will be tested in a few environments:

  • NgZone + ChangeDetection.Default

  • NgZone + ChangeDetection.OnPush

  • Zoneless + ChangeDetection.Default

  • Zoneless + ChangeDetection.OnPush

Environment

  • Angular 18.0.7

  • zone.js 0.14.3

  • Rxjs 7.8.0

Click event

Let’s start with a simple one. Button events are one of the most frequent events of any application. They are used for web page interactivity — navigation, form validation, animation triggering, etc.

NgZone + Default

NgZone + OnPush

Zoneless + Default

Zoneless + OnPush

Summary

From these tests, we can see that the change detection behavior between NgZone and Zoneless is not different, and to reduce the number of checks it is necessary to use a detection strategy via OnPush

Input + NgModel

Another one of the popular elements of any web application — search, login form, filtering data in a table. Listening and validating element values can be done in different ways from addEventListener to SubmitEvent, but nevertheless you should always listen. For these examples we use the ngModel binding

NgZone + Default

NgZone + OnPush

Zoneless + Default

Zoneless + OnPush

Summary

Similar to the button and click event tests, there is no difference between NgZone and Zoneless and OnPush should be used to get a better and more optimized version of the application

AsyncPipe

One of the older Angular APIs for handling asynchronous events and template updates. RxJS and AsyncPipe make it easier to handle asynchronous data flows, make code cleaner and more reactive, and improve the manageability of Angular applications.

From a component update perspective, there is not much difference between subscribing inside a component with a call to markForCheck() and using AsyncPipe. We have a new value — we need to request an update.

For this example we will use a simple timer() counter with a period of 1 sec and display the value on the screen.

NgZone + Default

NgZone + OnPush

Zoneless + Default

Zoneless + OnPush

Summary

Again no change between Zoneless and NgZone, still need to use OnPush to reduce the number of component tree checks

Signal

A new API for handling asynchronous values on a pub/sub basis.

Angular Signals is a system that keeps detailed track of how and where your state is used in the application, allowing the framework to optimize rendering updates.

The familiar timer() function will be used as the data source, with conversion to the Signal API via the toSignal() function

NgZone + Default

NgZone + OnPush

Zoneless + Default

Zoneless + OnPush

Summary

Finally we can evaluate and see the impact of signals on change detection checking for the whole application.

The event detection cycle for the entire component tree only works in the NgZone + Default mapping. In all other cases, changes coming from signal are isolated for the component and do not require checking even the component itself. Wow 🔥

Web API — setInterval

The basic idea behind zone.js is to create an execution context, called a “zone”, that can intercept and handle all asynchronous operations occurring in that context. Zones can intercept and handle all asynchronous operations occurring in them. This is accomplished by overriding standard functions such as setTimeout, setInterval, promises, etc.

In simple language, “overriding” a function is monkey-patching. This is how zone.js notifies Angular to check for a change.

setInterval is one of the global functions available when code is executed in the browser. It is considered good practice to wrap the setInterval call in zone.runOutsideZone() to avoid unnecessary validation loops

NgZone + Default

NgZone + OnPush

Zoneless + Default

Zoneless + OnPush

Summary

The tests give an unexpected picture, the use of setInterval has a significant impact on component tree checking only when using the NgZone + Default mapping. In other cases, using setInterval is safe without using runOutsideZone, and does not affect component tree checking.

Conclusion

It is relatively safe to use Zoneless in current applications when upgrading to version 18. All necessary component checks and trigger responses are similar between Zone.js and Zoneless. If possible, you should always follow the event detection strategy via OnPush.

Signal’s were a pleasant surprise. Template updates are isolated and do not affect the component tree, which will be extremely useful for partial hydration.

There is no need to be afraid of using asynchronous global Web API. Even when using Zoneless + Default — it doesn’t cause component tree checking and all calculations happen in the background.

I realize I have only checked a small part of possible scenarios and triggers, there are many more examples — using Signal-input / output / model, Signal Store, etc.

Bonus

One of the benefits of dropping ZoneJs will be to lighten the current build.

If your standard application with 0 logic at the start is 216kb in size, ZoneJs takes up 15% of the total build, which is quite a large share

After uninstalling ZoneJs got rid of 33kb and made your build lighter, and the app continued to work as before.

Top comments (3)

Collapse
 
jangelodev profile image
João Angelo

Hi Maksim Dolgih,
Top, very nice and helpful !
Thanks for sharing.

Collapse
 
vkinsane profile image
Vishal Khandate

Have you noticed any practical challenges when transitioning existing projects to use these new features? 🤔 Also, how does this impact the overall developer experience in terms of debugging and maintenance?

Collapse
 
misterion96 profile image
Maksim Dolgih • Edited

I haven't noticed yet. But it is worth noting that projects have always followed "best practices". I haven't had any places using the Zone API, which is removed and confirmed by the Angular team

angular.dev/guide/experimental/zon...

Also the code that used runOutsideAngular works without problems, although it is redundant in Zoneless

export function ngDebounce<T extends unknown[]>(
  callback: TNgDebounceFunction<T>,
  delay: number = 300,
  injector?: Injector,
): TNgDebounceFunction<T> {
  let timeout: TTimer;
  const zone: NgZone = injector?.get(NgZone) || inject(NgZone);

  return (...args: T): void => {
    if (timeout) {
      clearTimeout(timeout);
    }

    timeout = zone.runOutsideAngular(() =>
      setTimeout(() => {
        callback(...args);
      }, delay),
    );
  };
}
Enter fullscreen mode Exit fullscreen mode