In this configuration angular requires zone js 

Angular developers owe a lot to the zone.js library. For example, it helps achieve an almost magical ease in working with Angular. In fact, almost always, when we just need to change some property, and we change it without thinking about it, Angular re-renders the corresponding components. As a result, what the user sees always…

Angular developers owe a lot to the zone.js library. For example, it helps achieve an almost magical ease in working with Angular. In fact, almost always, when we just need to change some property, and we change it without thinking about it, Angular re-renders the corresponding components. As a result, what the user sees always contains the most up-to-date information. This is just great.

Here I would like to explore some aspects of how using the new Ivy compiler (it appeared in Angular 9) can make it much easier to abandon zone.js.

By doing away with this library, I was able to significantly improve the performance of a high-load Angular application. At the same time I was able to implement the necessary mechanisms using TypeScript decorators which resulted in not a huge waste of system resources.

Note that the approach to optimizing Angular applications presented in this material is only possible because Angular Ivy and AOT are enabled by default. This article is written for educational purposes, it is not intended to advocate the approach presented in it for developing Angular projects.

Why would you want to use Angular without zone.js?

Before we go any further – let’s ask one important question: “Is it worth getting rid of zone.js, given that this library helps us perform, effortlessly, pattern re-rendering?” Certainly, this library is quite useful. But, as usual, you have to pay for everything.

If your application has specific performance requirements, disabling zone.js can help meet those requirements. As an example of an application where performance is critical, you might have a project where the interface is updated very frequently. In my case, that would be a real-time trading application. Its client receives messages via WebSocket protocol all the time. The data from these messages should be displayed as soon as possible.

Getting zone.js out of Angular

It’s very easy to make Angular work without zone.js. To do this, you first need to comment out or delete the corresponding import command, which is in the polyfills.ts file.

Commented out the import zone.js command

Next – you need to equip the root module with the following options:

platformBrowserDynamic()
  .bootstrapModule(AppModule, {
    ngZone: 'noop'
  })
  .catch(err => console.error(err));

Angular Ivy: self-detecting changes with ɵdetectChanges and ɵmarkDirty

Before we can start building the TypeScript decorator, we need to learn about how Ivy allows us to trigger the change detection process of a component by making it “dirty”, while bypassing zone.js and DI.

We now have two additional features exported from @angular/core. These are ɵdetectChanges and ɵmarkDirty. These two functions are still for internal use and are unstable – they have the symbol ɵ at the beginning of their names.

Let’s look at how you can use these functions.

Function ɵmarkDirty.

This function allows you to mark a component by making it “dirty”, that is, in need of re-rendering. It, if the component was not marked as “dirty” before it was called, will schedule the change detection process to start.

import { ɵmarkDirty as markDirty } from '@angular/core';
@Component({...})
class MyComponent {
  setTitle(title: string) {
    this.title = title;
    markDirty(this);
  }
}

EdetectChanges function

Angular’s internal documentation says that, for efficiency reasons, you should not use function ɵdetectChanges. It is recommended to use function ɵmarkDirty instead. The function ɵdetectChanges synchronously calls the process of detecting changes in a component and its subcomponents.

import { ɵdetectChanges as detectChanges } from '@angular/core';
@Component({...})
class MyComponent {
  setTitle(title: string) {
    this.title = title;
    detectChanges(this);
  }
}

Automatic change detection with the TypeScript decorator

Although the features provided by Angular make development more convenient by bypassing the DI, it can still be frustrating for the programmer to have to import and call those features himself to trigger the change detection process.

To simplify the automatic triggering of change detection, we can write a TypeScript decorator, which will do this job by itself. Of course there are some limitations, which we will talk about below, but in my case this approach was exactly what I needed.

Familiarizing myself with the @observed decorator

In order to detect changes with as little effort as possible, we create a decorator that can be applied in three ways. Specifically, it applies to the following entities:

  • To synchronous methods.
  • To Observable objects.
  • To regular objects.

Let’s look at a couple of small examples. In the following code snippet, we apply the @observed decorator to the state object and the changeTitle method:

export class Component {
    title = '';

    @observed() state = {
        name: ''
    };

    @observed()
    changeTitle(title: string) {
        this.title = title;
    }

    changeName(name: string) {
        this.state.name = name;
    }
}
  • To check for changes to the state object, we use a Proxy object that intercepts the object’s changes and calls the change detection procedure.
  • We override the changeTitle method by applying a function that first calls this method and then starts the change detection process.

And here is an example with BehaviorSubject:

export class AppComponent {
    @observed() show$ = new BehaviorSubject(true);

    toggle() {
        this.show$.next(!this.show$.value);
    }
}

In the case of Observable objects, the application of the decorator looks a bit more complicated. Namely, we need to subscribe to the observed object and mark the component as “dirty” in the subscription, but we also need to clean up the subscription. To do this, we reassign ngOnInit and ngOnDestroy for the subscription and for its subsequent clearing.

Creating a decorator

Here’s the decorator signature:

export function observed() {
  return function(
    target: object,
    propertyKey: string,
    descriptor?: PropertyDescriptor
  ) {}
}

As you can see, descriptor is an optional parameter. This is so because we need the decorator to be able to apply to both methods and properties. If the parameter exists, it means that the decorator applies to a method. In that case this is what we do:

  • We save the descriptor property. value.
  • We override the method as follows: We call the original function, then we call markDirty(this) to start the change detection process. This is what it looks like:
if (descriptor) {
  const original = descriptor.value; // сохраним исходные данные
  descriptor.value = function(...args: any[]) {
    original.apply(this, args); // вызовем исходный метод
    markDirty(this);
  };
} else {
  // проверим свойство
}

Next, we need to check what type of property we are dealing with. It can be an Observable object or a regular object. Here we are going to use another Angular API. I guess it’s not meant to be used in normal applications (sorry!).

We’re talking about the ɵcmp property which gives us access to the properties Angular handles once they are defined. We can use them to override the component’s onInit and onDestroy methods.

const getCmp = type => (type).ɵcmp;
const cmp = getCmp(target.constructor);
const onInit = cmp.onInit || noop;
const onDestroy = cmp.onDestroy || noop;

To mark a property as one that will be monitored, we use ReflectMetadata and set its value to true. As a result, we will know that we need to observe the property when the component is initialized:

Reflect.set(target, propertyKey, true);

Now it’s time to redefine the onInit hook and check the properties when creating an instance of the component:

cmp.onInit = function() {
  checkComponentProperties(this);
  onInit.call(this);
};

Let’s define a checkComponentProperties function that will bypass the component properties, filtering them by the value previously set with Reflect.set:

const checkComponentProperties = (ctx) => {
  const props = Object.getOwnPropertyNames(ctx);

  props.map((prop) => {
    return Reflect.get(target, prop);
  }).filter(Boolean).forEach(() => {
    checkProperty.call(ctx, propertyKey);
  });
};

The checkProperty function will be responsible for decorating the individual properties. First we check if the property is an Observable object or a regular object. If it is an Observable-object, we subscribe to it and add the subscription to the subscription list stored in the component for its internal needs.

const checkProperty = function(name: string) {
  const ctx = this;

  if (ctx[name] instanceof Observable) {
    const subscriptions = getSubscriptions(ctx);
    subscriptions.add(ctx[name].subscribe(() => {
      markDirty(ctx);
    }));
  } else {
    // проверим объект
  }
};

If the property is a normal object, we convert it to a Proxy object and call markDirty in its handler function:

const handler = {
  set(obj, prop, value) {
    obj[prop] = value;
    ɵmarkDirty(ctx);
    return true;
  }
};

ctx[name] = new Proxy(ctx, handler);

Finally, you need to clear the subscription after destroying the component:

cmp.onDestroy = function() {
  const ctx = this;
  if (ctx[subscriptionsSymbol]) {
    ctx[subscriptionsSymbol].unsubscribe();
  }
  onDestroy.call(ctx);
};

The possibilities of this decorator cannot be called comprehensive. They do not cover all the possible applications that can be found in a large application. For example, these are calls to template functions that return Observable objects. But I’m working on that.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *