When I use the word change detection, this is not specific enough in Angular. It can be referred as change detection cycle, or it can be referred as change detection job. The cycle refer to the process of recursive updating the hierachy of angular views. In the context of each component, the job refers to the work of comparing the current state and previous state of the component, updating the view of the component when there is a difference. In the last post, I mentioned. Zone.js will call applicationRef.tick(), and it will start a change detection cycle. During the cycle, the job can happen depending on some condition. But in the cycle, what does it actually do? This is a pretty complicated process. Here is some very high-level pseudo code, and it omits lots of details, let’s just focus on the high-level process.

function application_ref_tick() {
  refreshView(rootView);
}

function refreshView(view) {

   if (view.template_function && (view.always_check || view.is_dirty)) {
    // aka dirty checking, change detection
    view.tempalte_function(flags.update);
  }

  // ngOnInit, ngDoCheck, ngOnChanges for child views
  execute_pre_order_hooks_of_child_components(view);

  // ngOnConentInit, ngOnContentChecked for child views
  execute_content_hooks_of_child_components(view);

  for (let view of view.Children) {
    if (view.always_check || view.is_dirty) {
      refreshView(view);
    }
  }

  // ngAfterViewChecked for child views
  exeucte_view_check_hooks(view);
}

view.detectChanges() = {
  refereshView(view);
}

In Angular runtime, each component instance is associated with a lview array and tview array. Let’s simplify with a view object. What do we take away from the code?

  1. the refreshView function calls itself recursively from the top view to the leaf views.

  2. The actual change detection job (aka dirty-checking) is calling the template function in update mode, it may or may not happen in the cycle.

  3. For each view, its life cycle hooks are called when refreshing its parent view.

  4. Change detection is executed only if a view is marked always_check, is_dirty.

  5. If a view is not marked always_check or is_dirty, its parent view will not refresh it. But its life cycle hooks are still being called by its parent view.

  6. When a cycle start, if all components are marked always checked, all the cycle hooks and all the template functions of all components will be called.

It seems to be very low efficiency. But in a lot of cases, it is ok, because this cycle is running very fast. The following cases can make an angular app very slow.

  1. The component tree gets bigger, like thousands of them
  2. There are some events triggered very fast, such as keyboard events
  3. Some setTimeIntervals fired aggressively.
  4. The DOM update requires a heavy repaint.

So let’s revisit the best practices in the first post and the rationale behind them.

Why Use OnPush Component everywhere?

OnPush component is the component with always_check flags equal to false.

When Angular binds events such as click, keypress to a model method, Angular wraps the handler first in a higher order function. So that when those events trigger, angular will the following method first.

markViewDirty(startView);

What it does is to find the start view inside which the event is generated, and mark the start view and its ancestors dirty. If a OnPush component is not in this hierarchy, the component is not dirty-checked.

In the following examples, all the components are OnPush components. Click on the tick() button on all components, and pay attention to the output of the console.

  1. click on child2, it will only trigger dirty checking on root, and child2.
  2. click on child1, it will only trigger dirty checking on root, and child1
  3. click on grandson, it will only trigger dirty checking on root, and child1, and grandson.

Why use observable everywhere?

In my first post of this series, I showcase that after changing a component to OnPush, all the state changes inside setTimeout, setInterval, and promise will not be detected anymore? Isn’t that Zone.js all ready monkey-patched them to trigger the change detection cycle? Yes, they did trigger the cycle. But in the cycle, they are not dirty-checked, because the view is OnPush. Unlike event handlers for DOM events, such as click, Angular cannot patch the callback in these api to mark the containing view dirty. That is why containing view is not dirty-checked.

The proposed solution is that use observable? So the setInterval is changed to interval observable. But this observable is still based on setInterval, if we manually subscribe it, the state change inside the call back will not be dirty-checked. This is the same for observable created by httpClient.

So the solution is to use async pipe to subscribe observables in the template directly. Why this jobs. Here is a code snippet from async` pipe

private _updateLatestValue(async: any, value: Object): void {
  if (async === this._obj) {
    this._latestValue = value;
    // Note: `this._ref` is only cleared in `ngOnDestroy` so is known to be available when a
    // value is being updated.
    this._ref!.markForCheck();
  }
}

It jobs because it marks the view as dirty after subscription. Our manual subscription of observable will also job when we add the call markForCheck()`.

Why separate big components into smaller ones?

Besides the benefit from the design perspective, this can reduce change detection. If lots of widgets live in the same component, every change detection cycle caused by one widget will cause the change detection of all other widgets.

Grouping different widgets in OnPush components will reduce this unnecessary change detection for other widgets

Why “Micro” Optimization?

In the timer examples, although the interval observable does job, it still causes a lot of change detection cycles, which can affect the whole component tree. The best way to optimize it is to run the setTimeout outside of Zone.js, and call cd.detectChanges() to trigger local dirty checking. By doing this we can avoid triggering any change detection cycle. Here is the factory app.