Change Detection Basics
How Angular decides when to re-render the DOM, and why the default and OnPush strategies behave differently.
3 min de lectura
Every time you change a component property, something has to notice and update the DOM to match. That process — figuring out what changed and re-rendering only what needs it — is change detection, and understanding roughly how it works explains a surprising number of "why isn't my UI updating" bugs.
The default strategy
By default, Angular's change detection runs whenever it suspects anything in the app might have changed — a click, an HTTP response, a timer firing, an observable emitting. It then walks the entire component tree from the root down, comparing each template expression's current value to what it rendered last time, and patching the DOM wherever a value differs.
Historically, this "anything might have changed, check everything" trigger was handled by a library called Zone.js, which patches asynchronous browser APIs (setTimeout, event listeners, promises) so Angular can hook into essentially every point where new data might arrive. It's what makes the default experience feel automatic — you rarely have to tell Angular "something changed, please re-render."
Why that isn't free
Checking the entire component tree on every possible event is thorough but wasteful once an app has hundreds of components: most of them didn't actually change, but Angular still evaluates every template expression in every one of them to be sure. In a large, deeply nested app, this can become a measurable performance cost.
OnPush: opting into fewer checks
import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
@Component({
selector: 'app-task-item',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<li [class.done]="task.completed">{{ task.title }}</li>`,
})
export class TaskItemComponent {
@Input() task!: { title: string; completed: boolean };
}An OnPush component tells Angular: only re-check me when one of my @Input references changes (a new object entirely, not a mutated property on the existing one), an event originates from inside me, or an observable I'm subscribed to with the async pipe emits. Angular skips checking this component — and everything under it — on updates that don't meet one of those conditions, which is where the performance benefit comes from.
The catch is in that parenthetical: OnPush compares inputs by reference. Mutating an object in place (task.completed = true) doesn't change which object task points to, so an OnPush component won't notice — you'd need to pass a new object ({ ...task, completed: true }) for the change to register. This is the single most common OnPush bug: a value visibly changed, but the component using OnPush never re-rendered because the reference stayed the same.
How signals change this picture
Because a signal tracks exactly which template expressions read it, a component built around signals doesn't need Zone.js's "something happened, check everything" model at all — Angular can update precisely the DOM nodes that depend on a signal that changed, regardless of the component's change detection strategy. This is a large part of why Angular has been investing in signals: they sidestep the reference-equality gotchas of OnPush while still avoiding the cost of checking an entire tree indiscriminately.
What to take from this as a beginner
Angular's default behavior works correctly without you thinking about any of this — reach for OnPush (or signal-based state) as a deliberate performance optimization once you've noticed an actual slowdown, not as a default setting for every component from day one.