Codementor Events

Optimizing Change Detection in Angular 2+ By Example

Published Jul 29, 2017
Optimizing Change Detection in Angular 2+ By Example

One of the things Angular boasts is having automatic change detection by default, meaning that when an object or object property is changed at runtime, the change will be reflected in the view without having to explicity set up any manual mechanism for doing so. In version 2 and above, Angular has attempted to empower developers with more control over how and when to check for changes. I demonstrate this in an example from a class I taught online, which I would like to share, in hope that it will give you more of a concrete understanding of this idea.

Change detection in Angular 1

First, let's see how Angular 1 accomplished the same thing. For each expression in the app, a watcher would be assigned when registered on the $scope, then each time a change occured, Angular would run through the list of every watcher to make sure the value returned from the expression has not changed. This was described as a digest cycle and for each change found in the list, another digest cycle would run until it successful pass through the list yielding no changes. This approach has a few downsides, however:

  • If an expression has a changing value each digest cycle, for example, if you accidentally created a new object each time the expression was evaluated, you would eventually receive this all too familiar error:
10 $digest() iterations reached. Aborting!

Angular would throw an error in, after 10 digest cycles, to prevent an infinite loop.

  • More pertient to this post, there's no guaranteed order to which the watchers would run, so you could potentially run into a situation where change detection in a child direcive/component would be evaulated before its parent, leading to some weird results. In a visual tree structure, change detection may very well end up traversing each node like so:

PgCQycv.png

Happy Trees

BobRoss1.jpg

Since Angular 2+, apps are by definition built with a nested tree structure, starting with the root component. Therefore, no more potiential for the "chicken or egg" dilemma in terms of change detection between parent/child components.

Angular 2+ change detection uses a directional tree graph, which basically means changes are guaranteed to propogate unidirectionally, and will always traverse each component instance once starting from the root.

6giapZA.png

Sounds great, what's the issue?

The directed tree graph makes things much faster, and this built-in behavior will usually be the only thing you'll need in your Angular 2+ app. However, since the focus of this post is optimization, our question would be: What happens when a node (nested component) further down the tree registers a change?

r26Z31H.png

As mentioned above, Angular will always traverse each component instance once, starting from the root. Additionally, since JavaScript does not have object immutability, Angular must be conservative and check to make sure that each component instance hasn't changed since change detection was last run.

Fs2GVDB.png

What if we only want to run change detection under certain circumstances? There's actually a few ways to do this, but we'll cover the most basic one below.

Learning by example

Let's take a look at a small sample app that lists movies of different categories (New, Upcoming, Top Rated, etc). The MovieDetailsComponent takes an @Input() object to represent the movie list we want details for.

import {
  Component,
  Input,
  ...
} from '@angular/core';

@Component({
  selector: 'movie-details',
  ...
})
export class MovieDetailsComponent {
  @Input('movie') movieData: Movie;
  ...
}
<movie-details
  [movie]="movie"
  *ngFor="let movie of movies">
</movie-details>

The Movie type is an interface with movie detail-related properties, as well as a flag labeled markedToSee.

export interface Movie {
  id: number;
  title: string;
  release_date: string;
  overview: string;
  ...
  markedToSee: boolean;
}

Let's alias the movie object to the variable movieData inside the MovieDetailsComponent so we can "hook into" the movie object being called and insert a statement to log to the console. ES6 allows us to use the get syntax to "bind an object property to a function that will be called when that property is looked up".

import {
  Component,
  Input,
  ...
} from '@angular/core';

@Component({
  selector: 'movie-details',
  ...
})
export class MovieDetailsComponent {
  @Input('movie') movieData: Movie;

  get movie() {
    console.log(`GET movie: ${this.movieData.title}`);
    return this.movieData;
  }
  ...
}

In our list of movies, we have a link at the top that, when clicked, will randomly select a movie for us to see. This will be marked with some text and a background highlight color. Also, we can see the change detection being triggered on every MovieDetailsComponent instance when the markToSee flag is flipped on "only one movie".

The event function tied to this action looks like this:

  // In parent component
  pickMovie(event: any) {
    event.preventDefault();
    let movie: Movie = this.movies.find(movie => movie.markedToSee);
    // If a movie was already marked to see, set the flag back to false
    if (movie) {
      movie.markedToSee = false;
    }
    // Mark a random movie to see, mark the flag as true
    this.movies[Math.floor(Math.random() * this.movies.length)].markedToSee = true;
  }

And testing it out in the browser...

LbvGLbe.gif

We see that both times we clicked Pick random movie to see, it called the getter function for the movie object of every MovieDetailsComponent. It also called the getter multiple times for each property binding in the view (title, overview, etc), but that's just a side note. Ideally, we only want to see the getter function called on one or two of the components (one if it's the first time we're picking a movie to see, two if we've clicked it again and
the markedToSee flag has been flipped off for the old movie and flipped on for the new movie).

Taking change detection into our own hands (sort of)

We can tell Angular to be more aggressive about deciding when to use change detection by changing the ChangeDetectionStrategy. We are using the default value right now, which will always check for updates on a component – if we explicitly mark it as OnPush, it will only run change detection when:

  • The reference to an @Input() object is changed
  • An event is triggered internally within the component
import {
  Component,
  Input,
  ChangeDetectionStrategy,
  ...
} from '@angular/core';

@Component({
  selector: 'movie-details',
  changeDetection: ChangeDetectionStrategy.OnPush,
  ...
})

IT BROKE!!!!!!!

a.gif

Sure enough, if you go and check in the browser again, you'll see that "pick random movie to see" is borken — it no longer chooses a movie for us and we don't see the logger statements in the console either, which is another verification. markedToSee is a property on the movie object, and the movie object is an @Input() property, so the object "changed" right?

Well, yes and no. The object itself changed but the reference to the object didn't change, it's still the same object. As mentioned above, change detection will only occur if the object reference changes. How can we tweak this so we will get the results we want?

An easy way is to look at the markedToSee property itself. This logic doesn't really belong in the Movie type as it relates to view logic and user-specific logic, so what if we moved it out into a separate @Input() property on the MovieDetailsComponent (since it's the parent component's responsibility for updating that flag anyways)?

First the component...

  ...
  export class MovieDetailsComponent {
    @Input('movie') movieData: Movie;
    @Input() markedToSee: boolean;
    ...
  }

Then the HTML tag...

  <movie-details
    [movie]="movie"
    [markedToSee]="index === selectedMovieIndex"
    *ngFor="let movie of movies; let index = index">
  </movie-details>

And finally the function for updating the markedToSee movie...

// In parent component
...
export class MoviesComponent {
  selectedMovieIndex: number;

  pickMovie(event: any) {
    event.preventDefault();
    this.selectedMovieIndex = Math.floor(Math.random() * this.movies.length);
  }
}

Success!

b.gif

We see that the first time the link is clicked, only one component instance is triggered for update. Then, on each subsequent click of the link, we see change detection triggered for the previously selected movie as well
as the newly selected movie. This greatly reduces the number of checks that are needed for a relatively simple change.

Also, if our MovieDetailsComponent had nested components within it as child components, we could hypothetically skip entire subtrees while doing change detection by using ChangeDetectionStrategy.OnPush.

y3mafMD.png

Further reading

Finally, as I mentioned previously, there are a few ways to take more control over Angular's change detection, and we covered one of them. For greater control over the individual change detection mechanism of a component instance, check out the documentation for ChangeDetectorRef.

Conclusion

Angular 2 and above give us greater control over change detection than we've ever had with the Angular ecosystem. It is truly a major boost from simply running through a huge list of watchers every time a change is made. Though the default mechanism is already pretty fast, we can further tune our change detection engine when and where it's needed. The example I used leaves out a lot of code for the sake of this article, but you can find the full source code as part of an online Angular 2+ class I taught here.

Happy coding! 😃


This post was originally published by the author here. This version has been edited for clarity and may appear different from the original post.

Discover and read more posts from Kevin Farst
get started
post commentsBe the first to share your opinion
Show more replies