Optimizing Angular Performance with OnPush Change Detection
Learn how to use ChangeDetectionStrategy.OnPush in Angular to reduce unnecessary change detection cycles, improve performance, and implement immutable data patterns.
08 Jul 2026, 19:43 UTC

Reducing Change Detection Overhead
In large Angular applications, the default change detection strategy can become a performance bottleneck. By default, Angular checks every component in the tree whenever any asynchronous event occurs—such as a timer, a promise resolution, or a click event—regardless of whether that component's data actually changed.
The solution is ChangeDetectionStrategy.OnPush. This strategy instructs Angular to skip change detection for a component and its entire subtree unless specific triggers occur. This significantly reduces the number of checks per cycle, improving frame rates and responsiveness in data-heavy interfaces.
How OnPush Triggers Updates
When a component is marked as OnPush, Angular only runs change detection for it in the following scenarios:
- Input Reference Change: An
@Input()property receives a new reference. This is a shallow check; if you mutate a property inside an object, the reference remains the same, and the view will not update. - Event Origin: An event handler (like
(click)) is triggered from within the component or one of its children. - Async Pipe: An
| asyncpipe in the template receives a new value from an Observable. The pipe internally callsmarkForCheck(). - Manual Trigger: The developer explicitly calls
ChangeDetectorRef.markForCheck()ordetectChanges().
Implementation Example
To implement OnPush, set the changeDetection property in the @Component decorator. The following example demonstrates a presentational component that relies on immutable data patterns.
import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
interface UserProfile {
name: string;
email: string;
}
@Component({
selector: 'app-user-profile',
template: `
<div class="profile-card">
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
</div>
`,
// This tells Angular to only check this component when inputs change
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserProfileComponent {
@Input() user!: UserProfile;
}For the view to update, the parent component must provide a new object reference rather than mutating the existing one:
// Parent Component Logic
updateUserName(newName: string) {
// WRONG: this.user.name = newName; (View will not update)
// RIGHT: Create a new object reference using the spread operator
this.user = { ...this.user, name: newName };
}Limitations and Engineering Trade-offs
The primary limitation of OnPush is that it ignores mutations. If you push an item into an array (this.items.push(newItem)), Angular will not detect the change because the array reference is identical. You must replace the array entirely: this.items = [...this.items, newItem].
This introduces a requirement for immutability. While this leads to more predictable state management, it increases memory allocation frequency as new objects are created. In most web applications, the performance gain from skipping change detection far outweighs the cost of object allocation.
When working with third-party libraries that mutate data internally, you cannot rely on OnPush automatically. In these cases, you must inject ChangeDetectorRef and manually call markForCheck() inside the library's callback function to notify Angular that the component needs to be checked in the next cycle.
Common Implementation Pitfalls
- Mutating Inputs: Expecting a view to update after changing a property of an object passed via
@Input(). - Overusing ngDoCheck: Implementing deep equality checks inside
ngDoCheckto bypassOnPushlimitations. This defeats the purpose of the strategy by running expensive logic on every detection cycle. - Ignoring the Async Pipe: Manually subscribing to Observables in the TypeScript class and forgetting to call
markForCheck(). Using the| asyncpipe is the recommended pattern as it handles the check trigger automatically.
Verifying the Performance Gain
To verify that OnPush is working as intended, you can use the following methods:
- Console Logging: Add a
console.log('Checked')inside thengDoCheck()lifecycle hook. Compare the number of logs triggered by a global event (like a timer) between a default component and anOnPushcomponent. - Angular DevTools: Use the Profiler tab in Angular DevTools. Record a user interaction and observe the "Change Detection" bars.
OnPushcomponents will appear as skipped (greyed out) if their inputs didn't change, whereas default components will show as processed. - Reference Test: Bind an object to an
OnPushcomponent. Mutate a property of that object; the UI should remain static. Replace the object with a new reference; the UI should update immediately.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.