Angular's default change detection checks every component on every tick — genuinely wasteful for a large component tree where most components rarely actually change; OnPush narrows down exactly when a specific component gets re-checked, rather than disabling the mechanism entirely.
The default: Angular's automatic change detection
By default, every Angular component gets checked for changes on virtually every possible trigger — a DOM event anywhere in the app, an HTTP response, a timer firing — regardless of whether that specific component's own data actually changed; this is simple and always correct, but it means significant unnecessary checking work in a large application.
Enabling OnPush
@Component({
selector: 'app-product-card',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `...`
})
export class ProductCardComponent {
@Input() product!: Product;
}
What actually triggers a re-check under OnPush
An OnPush component only re-checks when one of three things happens: an @Input() property receives a genuinely new reference (not just a mutated existing object), an event originates from within the component itself (a click, a form input), or an async pipe used in the component's own template emits a new value.
The critical gotcha: mutating an object doesn't trigger a re-check
// WRONG — mutating the existing array doesn't change its reference
this.products.push(newProduct);
// CORRECT — creates a genuinely new array reference
this.products = [...this.products, newProduct];
This is the single most common source of confusion with OnPush — since it checks for a new object *reference*, not a deep value comparison, mutating an existing array or object in place (as in the wrong example) is invisible to OnPush's change detection entirely, even though the actual data genuinely changed.
Why immutable state patterns pair naturally with OnPush
This is exactly why state management approaches favoring immutability (NgRx, or simply consistently creating new objects/arrays rather than mutating existing ones) work so well with OnPush — every genuine state change naturally produces a new reference, which is precisely the signal OnPush is designed to detect.
Manually triggering change detection when needed
constructor(private cdr: ChangeDetectorRef) {}
updateSomethingOutsideAngular(): void {
// some change that Angular's zone doesn't automatically know about
this.someValue = newValue;
this.cdr.markForCheck();
}
markForCheck() manually flags the component (and its ancestors) for the next check cycle — genuinely necessary for a change originating outside Angular's normal zone-tracked event handling, such as a raw setTimeout callback or a third-party library's own callback that Angular doesn't automatically know to react to.
Using the async pipe to avoid manual change detection entirely
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
template: `{{ product$ | async }}`
})
export class ProductCardComponent {
product$ = this.productService.getProduct(this.productId);
}
The async pipe automatically calls markForCheck() internally whenever its Observable emits — this is exactly why combining OnPush with the async pipe (rather than manually subscribing and assigning to a plain property) is the generally recommended pattern, since it avoids needing to manually manage change detection at all.
Measuring whether OnPush is actually worth adopting
OnPush genuinely matters for performance in a large component tree with frequent change detection cycles — for a small application, the default strategy's overhead is rarely noticeable, and adopting OnPush everywhere prematurely adds real complexity (the immutability discipline it requires) for a performance benefit that may not be needed yet.