This article is an excerpt from my Angular Deep Dive course

Angular provides two distinct strategies for managing change detection behavior at the component level. These strategies, defined in the compiler, are known as Default and OnPush:

export enum ChangeDetectionStrategy {
  OnPush = 0,
  Default = 1
}

These strategies dictate whether a child component gets checked when Angular processes change detection for its parent. The strategy assigned to a component applies to all nested directives, as they are evaluated alongside the host component. Once set, a strategy cannot be changed at runtime.

The default strategy, known internally as CheckAlways, means the component undergoes regular automatic change detection unless the view is explicitly detached. The OnPush strategy, internally called CheckOnce, skips change detection unless the component is flagged as dirty. Angular has built-in mechanisms to flag a component automatically, but you can also do it manually via the markForCheck method available on ChangeDetectorRef.

When you specify a strategy through the @Component() decorator, the Angular compiler stores it in the component's definition by way of the defineComponent function. For instance, with a component like this:

@Component({
  selector: 'a-op',
  template: `I am OnPush component`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class AOpComponent {}

the compiler generates a definition that appears as follows:

Deep dive into the OnPush change detection strategy in Angular — figure 1

When Angular creates an instance of the component, it uses this definition to assign the corresponding flag on the LView object representing the component's view:

Deep dive into the OnPush change detection strategy in Angular — figure 2

Consequently, every LView created for this component will carry either the CheckAlways or Dirty flag. For the OnPush strategy, the Dirty flag is cleared immediately after the first change detection pass.

These flags on LView are evaluated within the refreshView function when Angular decides whether to check a component:

function refreshComponent(hostLView, componentHostIdx) {
  // Only attached components that are CheckAlways or OnPush and dirty 
  // should be refreshed
  if (viewAttachedToChangeDetector(componentView)) {
    const tView = componentView[TVIEW];
    if (componentView[FLAGS] & (LViewFlags.CheckAlways | LViewFlags.Dirty)) {
      refreshView(tView, componentView, tView.template, componentView[CONTEXT]);
    } else if (componentView[TRANSPLANTED_VIEWS_TO_REFRESH] > 0) {
      // Only attached components that are CheckAlways 
      // or OnPush and dirty should be refreshed
      refreshContainsDirtyView(componentView);
    }
  }
}

Let's dig deeper into how each strategy operates.

Default strategy

With the default change detection strategy, a child component is always checked whenever its parent gets checked. The sole exception is if you detach the child's change detector explicitly:

@Component({
  selector: 'a-op',
  template: `I am OnPush component`
})
export class AOpComponent {
  constructor(private cdRef: ChangeDetectorRef) {
    cdRef.detach();
  }
}

The emphasis on the parent being checked is crucial. If the parent isn't checked, the child won't be either, even with the default strategy. This is because Angular handles a child's check as part of processing the parent.

Angular doesn't impose any particular pattern for detecting state changes, which is why it defaults to checking everything. A good example of an enforced pattern would be object immutability for @Input bindings—something the OnPush strategy relies on, as we'll see.

Here's a straightforward two-component hierarchy:

@Component({
  selector: 'a-op',
  template: `
    <button (click)="changeName()">Change name</button>
    <b-op [user]="user"></b-op>
  `,
})
export class AOpComponent {
  user = { name: 'A' };
 
  changeName() {
    this.user.name = 'B';
  }
}
 
@Component({
  selector: 'b-op',
  template: `<span>User name: {{user.name}}</span>`,
})
export class BOpComponent {
  @Input() user;
}

Clicking the button triggers an event handler where we modify user.name. During the subsequent change detection cycle, the child B component gets checked, and the UI updates:

Deep dive into the OnPush change detection strategy in Angular — figure 3

Even though the reference to the user object stayed the same—it was mutated internally—the new name still appears on screen. This illustrates why Angular checks all components by default. Without immutability guarantees, Angular can't determine if inputs changed and prompted an update to the component's state.

OnPush aka CheckOnce strategy

Although Angular doesn't force immutability, it offers a way to signal that a component's inputs are immutable, thereby reducing unnecessary checks. This comes via the OnPush change detection strategy, a common performance optimization. Internally called CheckOnce, it means change detection is paused for a component until marked dirty, then runs once, and pauses again. Marking can happen automatically or through markForCheck.

Let's revisit the earlier example, but now designate OnPush for the B component:

@Component({...})
export class AOpComponent {
  user = { name: 'A' };
 
  changeName() {
    this.user.name = 'B';
  }
}
 
@Component({
  selector: 'b-op',
  template: `
    <span>User name: {{user.name}}</span>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class BOpComponent {
  @Input() user;
}

After this change, Angular no longer detects the mutation in user.name:

Deep dive into the OnPush change detection strategy in Angular — figure 4

Notice that B is still checked during the initial bootstrap—it renders the starting name A. However, it's ignored in later change detection cycles, so clicking the button doesn't update the display from A to B. This happens because the reference to the user object passed down via @Input remains unchanged.

Before diving into how a component gets marked dirty, here are the scenarios Angular uses to test OnPush behavior:

should skip OnPush components in update mode when they are not dirty
should not check OnPush components in update mode when parent events occur

should check OnPush components on initialization
should call doCheck even when OnPush components are not dirty
should check OnPush components in update mode when inputs change
should check OnPush components in update mode when component events occur
should check parent OnPush components in update mode when child events occur
should check parent OnPush components when child directive on a template emits event

The final set of test cases confirms that automatic dirty marking happens under these conditions:

  • an @Input reference gets a new value
  • an event bound within the component itself fires

Let's examine each one.

@Input() bindings

In many cases, you only need to re-check a child when its inputs change—especially for pure presentational components fed solely through bindings.

Take our previous example:

@Component({...})
export class AOpComponent {
  user = { name: 'A' };
 
  changeName() {
    this.user.name = 'B';
  }
}
 
@Component({
  selector: 'b-op',
  template: `
    <span>User name: {{user.name}}</span>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class BOpComponent {
  @Input() user;
}

As shown earlier, clicking the button and mutating the name in the callback leaves the screen unchanged. This is because Angular does a shallow check on input parameters, and the user object's reference stays the same. Direct mutation doesn't produce a new reference, so the component isn't flagged dirty.

To make Angular notice the change, you must swap out the reference to user. Creating a new instance rather than modifying the old one will do the trick:

@Component({...})
export class AOpComponent {
  user = { name: 'A' };
 
  changeName() {
    this.user = {
      ...this.user,
      name: 'B',
    }
  }
}

And now it works as expected:

Deep dive into the OnPush change detection strategy in Angular — figure 5

You can enforce immutability on objects using a recursive Object.freeze approach:

export function deepFreeze(object) {
  const propNames = Object.getOwnPropertyNames(object);
 
  for (const name of propNames) {
    const value = object[name];
    if (value && typeof value === 'object') {
      deepFreeze(value);
    }
  }
 
  return Object.freeze(object);
}

Attempting to mutate the object then triggers an error:

Deep dive into the OnPush change detection strategy in Angular — figure 6

Alternatively, a library like immer is often the go-to solution:

import { produce } from 'immer';
 
@Component({...})
export class AOpComponent {
  user = { name: 'A' };
 
  changeName() {
    this.user = produce(this.user, (draft) => {
      draft.name = 'B';
    });
  }
}

That functions just fine too.

Bound UI events

When any native event fires on a component, Angular marks all its ancestors—up to the root—as dirty. The reasoning is that an event might trigger changes anywhere up the tree. Since Angular can't predict the impact, it erring on the side of caution and checks every ancestor.

Imagine a hierarchy of OnPush components like this:

AppComponent
    HeaderComponent
    ContentComponent
        TodoListComponent
            TodoComponent

If we bind an event listener within the TodoComponent template:

@Component({
  selector: 'todo',
  template: `
    <button (click)="edit()">Edit todo</button>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TodoComponent {
  edit() {}
}

Angular flags all ancestor components as dirty before running the event handler:

Deep dive into the OnPush change detection strategy in Angular — figure 7

So the set of components marked for a single check looks like this:

Root Component -> LViewFlags.Dirty
     |
    ...
     |
 ContentComponent -> LViewFlags.Dirty
     |
     |
 TodoListComponent  -> LViewFlags.Dirty
     |
     |
 TodoComponent (event triggered here) -> markViewDirty() -> LViewFlags.Dirty

In the next change detection run, Angular processes the full chain of TodoComponent's ancestors:

AppComponent (checked)
    HeaderComponent
    ContentComponent  (checked)
        TodosComponent  (checked)
            TodoComponent (checked)

Notice that HeaderComponent is skipped, as it isn't an ancestor of TodoComponent.

Manually marking components as dirty

Let's loop back to the example where we swapped the user object reference to update the name. That allowed Angular to spot the change and flag B automatically. Now, suppose we'd rather keep the same reference but still update the name. In that case, we mark the component dirty ourselves.

We can do this by injecting changeDetectorRef and calling its markForCheck method to tell Angular this component needs attention:

@Component({...})
export class BOpComponent {
  @Input() user;
 
  constructor(private cd: ChangeDetectorRef) {}
 
  someMethodWhichDetectsAndUpdate() {
    this.cd.markForCheck();
  }
}

What could serve as someMethodWhichDetectsAndUpdate? The NgDoCheck hook is an excellent fit. Angular runs it before checking the component, though it still happens during the parent's check. That's where we place our comparison logic and flag the component when we detect a discrepancy.

The choice to execute NgDoCheck even for OnPush components trips people up. But it makes sense once you realize it's part of the parent's change detection. Also note that ngDoCheck fires only for the top-most child. If that child has its own children and isn't checked, their ngDoCheck won't run.

Don't use ngDoCheck to log when a component is being checked. Instead, use an accessor function in the template like this {{ logCheck() }}.

Now, let's add our custom comparison inside NgDoCheck and mark the component dirty when we see a change:

@Component({...})
export class AOpComponent {...}
 
@Component({
  selector: 'b-op',
  template: `
    <span>User name: {{user.name}}</span>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class BOpComponent {
  @Input() user;
  previousUserName = '';
 
  constructor(private cd: ChangeDetectorRef) {}
 
  ngDoCheck() {
    if (this.user.name !== this.previousUserName) {
      this.cd.markForCheck();
      this.previousUserName = this.user.name;
    }
  }
}

Keep in mind that markForCheck doesn't itself trigger or guarantee a change detection run. For more on that, see the chapter on manual control.

Observables as @Inputs

Let's up the complexity. Suppose our child B component receives an RxJs-based observable that emits updates asynchronously—similar to what you'd see in an NgRx setup:

@Component({
  selector: 'a-op',
  template: `
    <button (click)="changeName()">Change name</button>
    <b-op [user$]="user$.asObservable()"></b-op>
  `,
})
export class AOpComponent {
  user$ = new BehaviorSubject({ name: 'A' });
 
  changeName() {
    const user = this.user$.getValue();
    this.user$.next(
      produce(user, (draft) => {
        draft.name = 'B';
      })
    );
  }
}

So we get a stream of user objects in the child. We need to subscribe, compare incoming values, and mark the component dirty when necessary:

@Component({
  selector: 'b-op',
  template: `
    <span>User name: {{user.name}}</span>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class BOpComponent {
  @Input() user$;
  user = null;
 
  constructor(private cd: ChangeDetectorRef) {}
 
  ngOnChanges() {
    this.user$.subscribe((user) => {
      if (user !== this.user) {
        this.cd.markForCheck();
        this.user = user;
      }
    });
  }
}

The logic inside ngOnChanges mirrors what the async pipe does internally:

export class AsyncPipe {
  transform() {
    if (obj) {
      this._subscribe(obj);
    }
  }
  
  private _updateLatestValue(async, value) {
    if (async === this._obj) {
      this._latestValue = value;
      this._ref!.markForCheck();
    }
  }
}

That's why the standard practice is to hand off the subscription and comparison work to the async pipe. The only caveat is that objects must stay immutable.

Below is the B component implementation using the async pipe:

@Component({
  selector: 'b-op',
  template: `
    <span>User name: {{(user$ | async).name}}</span>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class BOpComponent {
  @Input() user$;
}

There's a suite of test cases covering the async pipe's interaction with different types:

describe('Observable', () => {
	describe('transform', () => {
	  it('should return null when subscribing to an observable');
	  it('should return the latest available value');
	  it('should return same value when nothing has changed since the last call');
	  it('should dispose of the existing subscription when subscribing to a new observable');
	  it('should request a change detection check upon receiving a new value');
	  it('should return value for unchanged NaN');
	});
});
describe('Promise', () => {...});
describe('null', () => {...});
describe('undefined', () => {...});
describe('other types', () => {...});

This particular test addresses the scenario we've been discussing:

it('should request a change detection check upon receiving a new value', done => {
    pipe.transform(subscribable);
    emitter.emit(message);
 
    setTimeout(() => {
      expect(ref.markForCheck).toHaveBeenCalled();
      done();
    }, 10);
});

The pipe subscribes to the observable within its transform method and flags the component as dirty each time the observable pushes a new message.


If you're after more in-depth material along these lines, check out the course:

Deep dive into the OnPush change detection strategy in Angular — figure 8

If you believe something important is missing here do let me know in the comments!