Mastering OnPush A Guide to Angular Performance

Mastering OnPush A Guide to Angular Performance

By default, Angular is paranoid. Whenever a user clicks a button, a timer fires, or an HTTP request completes, Angular assumes the worst: everything might have changed. It traverses your entire component tree, from root to leaf, checking bindings.

For small apps, this is fine. For enterprise apps, it is a performance killer. The solution is ChangeDetectionStrategy.OnPush.

However, OnPush is not just a flag you toggle to make apps faster. It is a contract. If you enable it without understanding how it works, you will introduce bugs where the UI simply stops updating. This guide explains the rules of engagement.

The Mechanism

To understand why OnPush skips components, you need to understand the traversal algorithm:

How Angular Change Detection Works Internally →

1. The OnPush Contract

When you declare changeDetection: ChangeDetectionStrategy.OnPush, you are telling Angular: "Ignore this component (and its children) unless one of four specific things happens."

  • 1. Input Change: The reference of an @Input() property has changed.
  • 2. Event Fired: An event handler inside the component (e.g., (click)) was triggered.
  • 3. Async Pipe: An Observable subscribed via | async emitted a new value.
  • 4. Manual Trigger: You explicitly called ChangeDetectorRef.markForCheck().

If none of these occur, Angular will skip the component entirely during the change detection cycle. It essentially "sleeps."

2. The Immutability Trap

The most common reason OnPush breaks applications is Object Mutation.

Angular uses strict reference equality (===) to check inputs. It does not deep-check objects.

The Wrong Way (Mutation)

// Parent Component
updateUser() {
  this.user.name = 'Alice';
  // The object in memory is the same. 
  // The pointer hasn't changed.
  // Angular sees: oldUser === newUser.
  // RESULT: Child component does NOT update.
}

The Right Way (Immutability)

// Parent Component
updateUser() {
  this.user = { ...this.user, name: 'Alice' };
  // A new object is created at a new memory address.
  // Angular sees: oldUser !== newUser.
  // RESULT: Child component updates.
}

3. The Magic of the Async Pipe

If you use OnPush, the AsyncPipe is your best friend. It handles the manual work for you.

Internally, when the AsyncPipe receives a new value from an Observable, it calls markForCheck() automatically. This marks the path from the component up to the root as "dirty," ensuring the UI updates in the next cycle.

<app-user-profile [user]="user$ | async"></app-user-profile>

This pattern (Smart Container + Dumb Presentational Component) is the gold standard for Angular architecture.

4. markForCheck() vs detectChanges()

When you need to update the UI manually (e.g., after a setTimeout or a callback from a non-Angular library), you have two options in ChangeDetectorRef.

Method Behavior Use Case
markForCheck() Flags component for check in next cycle. Does not run immediately. Updating data from a service / observable manually.
detectChanges() Runs detection immediately on this component and its children. Detached views or extreme optimization scenarios.

Best Practice: Use markForCheck() 99% of the time. It aligns with the natural Angular cycle. detectChanges() is synchronous and expensive; using it too much negates the benefits of OnPush.

5. Signals: The "Perfect" OnPush

With Angular 16+, Signals make OnPush even easier. In fact, Signals are designed to work perfectly with the OnPush philosophy.

When you use a Signal in a template, Angular knows exactly when it changes. If you have an OnPush component that uses a Signal, updating that Signal will automatically mark the component for check. You don't need to call markForCheck() manually, and you don't need the AsyncPipe.

Future Proofing

See how Signals are replacing the need for manual change detection management:

Angular Signals vs Zone.js: The Complete Guide →

6. Verifying Your Strategy

How do you know if your OnPush strategy is working? You need to profile it. If you switch to OnPush but still see your component flashing "Green" in the Angular DevTools profiler when you click a button elsewhere, something is wrong (likely a Zone pollution issue).

Verification

Learn how to use the Flame Graph to confirm your components are "sleeping":

Profiling Angular Applications: The Ultimate DevTools Guide →

Conclusion

Switching to OnPush is the single most effective performance optimization you can make in an Angular application. It forces you to write better, more predictable code by enforcing immutability and explicit data flow.

It transforms your application from a "black box" that updates randomly into a precision machine that updates only when necessary.

About the Author
A
Arun Kumar Singh

arun009@gmail.com

Guest author at Thetechradar.info

Ready to Share Your Own Story?

Join our community of writers and share your expertise.

Submit Your Post