Angular  

Optimizing Angular Performance with OnPush Change Detection

Introduction

Performance is one of the most important parts of modern web applications. As Angular apps get bigger and more complex—especially large enterprise apps with dashboards, forms, and live data—the way the framework handles updates becomes very important. Efficient change detection ensures that the app stays fast and responsive. If it isn’t managed well, even small delays can add up, leading to slower screens and a poor user experience. That’s why optimizing change detection is key to keeping Angular applications smooth and reliable.

Understanding Angular Change Detection

Angular uses a system called Change Detection to keep the user interface (DOM) in sync with the application’s data. Whenever something in the app changes—like a user clicking a button, receiving an HTTP response, or a timer firing—Angular runs change detection to check if the view needs updating.

By default, Angular applies the ChangeDetectionStrategy.Default setting. With this approach:

  • Every component in the application is checked during each change detection cycle.

  • Any event, no matter how small, triggers a full check across the entire component tree.

This default behavior is simple and reliable, which makes it great for small or medium-sized applications. However, in large applications with complex user interfaces—such as enterprise apps with many nested components, dashboards, and real-time data streams—it can become inefficient. Running change detection everywhere for every event can slow down performance, leading to laggy interactions and wasted processing power.

That’s why developers often look for ways to optimize change detection, so Angular only updates the parts of the app that actually need it.

OnPush Change Detection Strategy

When you switch to ChangeDetectionStrategy.OnPush, Angular becomes more selective. Instead of checking every component on every cycle, Angular re-evaluates a component only when specific conditions are met:

  • Input reference changes – Angular checks the component if one of its @Input() properties receives a new reference (not just a mutated object).

  • Events triggered within the component – If a user interaction (like a click) occurs in the component or one of its children, Angular runs change detection for that subtree.

  • Observable or Signal updates – When data streams (via Observable, async pipe) or Angular Signals emit new values, the component updates accordingly.

  • Manual triggering using ChangeDetectorRef –

    • markForCheck() tells Angular to include the component in the next change detection cycle.

    • detectChanges() immediately runs change detection for the component and its subtree.

Default vs OnPush – Key Differences

FeatureDefault (Eager)OnPush
Change detection triggerAny eventInput reference change
PerformanceModerateHigh
Suitable forSmall appsLarge & scalable apps
Manual controlRarely neededOften needed

How to Use OnPush

To enable the OnPush strategy, configure the changeDetection property inside your component decorator. This tells Angular to run change detection for the component only when specific triggers occur (such as input reference changes, events, or observable emissions), improving performance by avoiding unnecessary checks.

import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
  selector: 'app-user-card',
  templateUrl: './user-card.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserCardComponent {
  @Input() user!: User;
}
  

With this setup, Angular will skip checking UserCardComponent unless its @Input() reference changes or another valid OnPush trigger occurs.

Why OnPush Improves Performance

Angular’s default change detection strategy is ChangeDetectionStrategy.Default. With this approach:

  • Angular traverses the entire component tree during every change detection cycle.

  • It checks all bindings, even if the underlying data hasn’t changed.

  • Any event—such as a click, an HTTP response, or a timer—triggers change detection across the whole application.

This works fine for smaller apps, but in large-scale applications with complex UIs, it can quickly become inefficient. The repeated checks consume unnecessary CPU cycles and lead to more DOM updates than needed, which can slow down performance.

By contrast, ChangeDetectionStrategy.OnPush changes the way Angular decides when to update a component:

  • Angular skips checking a component unless its input properties change or an observable emits new data.

  • This reduces the number of components Angular needs to process, lowering CPU usage.

  • It minimizes DOM updates, ensuring only the parts of the UI that actually need refreshing are updated.

  • As a result, applications become more scalable and responsive, even under heavy workloads.

OnPush is especially beneficial in scenarios such as:

  • Data-heavy dashboards with multiple widgets and charts.

  • Large tables with thousands of rows where frequent updates would otherwise be costly.

  • Real-time applications that continuously receive new data streams.

  • Enterprise Angular apps with deeply nested component structures and complex state management.

Important Concept: Immutability

OnPush works best when you use immutable data. This means you don’t directly change existing objects; instead, you create a new one when something changes.

Wrong Way (Mutation)

// Mutating the existing object
this.user.name = 'John';
  

Angular will not detect this change because the object reference remains the same.

Correct Way (New Reference)

// Creating a new object reference
this.user = {
  ...this.user,
  name: 'John'
};
  

Here, Angular detects the change because the object reference is new. By following immutability, Angular’s OnPush strategy can correctly identify updates, making apps more predictable, easier to debug, and more efficient—especially in large-scale applications.

Using OnPush with Observables

When working with OnPush change detection, one of the best practices is to use the async pipe in your templates.

<!-- Using async pipe with OnPush -->
<div *ngIf="user$ | async as user">
  {{ user.name }}
</div>
  

Why this works well:

  • The async pipe automatically subscribes to the observable and unsubscribes when the component is destroyed, preventing memory leaks.

  • It marks the component for check whenever new data is emitted, ensuring Angular updates the view correctly under OnPush.

  • It keeps the code clean and declarative, avoiding manual subscription management in the component class.

  • It fits perfectly with immutable data patterns, since each new emission creates a fresh reference that OnPush can detect.

Manual Change Detection (Advanced)

Even with OnPush, there are scenarios where Angular will not automatically detect changes—especially when updates occur outside Angular’s normal triggers (e.g., setTimeout, third-party libraries, manual DOM events, or non-reactive data updates).

In such cases, you can take manual control using ChangeDetectorRef.

Using markForCheck()

markForCheck() tells Angular: " This component should be checked in the next change detection cycle. "

It does not trigger detection immediately. Instead, it schedules the component to be checked when Angular runs the next cycle.

import { ChangeDetectionStrategy, ChangeDetectorRef, Component } from '@angular/core';

@Component({
  selector: 'app-user-card',
  template: `{{ data }}`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserCardComponent {
  data = '';

  constructor(private cd: ChangeDetectorRef) {}

  updateData(newValue: string) {
    this.data = newValue;
    this.cd.markForCheck(); // Schedule check for next cycle
  }
}

Best used when:

  • Updating state asynchronously

  • Integrating with third-party libraries

  • Updating data outside Angular’s zone

Using detectChanges()

detectChanges() runs change detection immediately for the component and its subtree.

this.cd.detectChanges(); // Immediately updates the view

This is more aggressive because it forces Angular to re-evaluate the component instantly.

Best used when:

  • You need immediate UI refresh

  • You're inside callbacks where Angular won't trigger detection

  • You detached change detection and want to manually control execution

Overusing markForCheck() or detectChanges() can defeat the purpose of OnPush by reintroducing frequent checks.

Manual change detection should be:

  • A targeted solution, not a default pattern

  • Used only when reactive patterns (Observables, Signals, immutable inputs) are not sufficient

Conclusion

ChangeDetectionStrategy.OnPush is one of Angular’s most effective tools for boosting performance. It reduces unnecessary checks, improves scalability, and promotes an immutable architecture. OnPush also works seamlessly with Observables and Signals, making reactive patterns more efficient. For developers building enterprise-grade applications, mastering OnPush is a key step toward creating fast, responsive, and maintainable Angular apps.