skip to content

In Angular 22, what is the difference between ChangeDetectionStrategy.OnPush and ChangeDetectionStrategy.Eager, and which does a component get by default?

level: juniorimportance: must knowfreq 82%

answer

  1. every pass versus on demand
  2. a dirty mark on the view
  3. what v22 changed
  4. the old name for Eager

basics

~20 s

An Eager component is refreshed every time a change detection pass reaches it; an OnPush component is refreshed only when its view has been marked dirty. Since Angular 22, a component that omits changeDetection is OnPush.

solid answer

~40 s

`ChangeDetectionStrategy.Eager` (the old `Default`, now a deprecated alias) makes Angular re-evaluate a component's template bindings every time a change detection pass reaches its view. `ChangeDetectionStrategy.OnPush` lets Angular skip that view unless it was marked: a template-bound input received a new reference, an Angular-bound event fired in it or a descendant, `markForCheck()` was called, or a signal read in its template changed. Since v22, a component that omits `changeDetection` is `OnPush`; you write `changeDetection: ChangeDetectionStrategy.Eager` to get the old behaviour, and the v22 `ng update` migration adds that line to existing components. Neither strategy starts a pass; each only decides what a pass that runs will refresh.

code

ts · 20 lines
ts
import {ChangeDetectionStrategy, Component, input} from '@angular/core';

// No changeDetection value: OnPush since Angular 22.
@Component({
  selector: 'app-order-total',
  template: `<strong>{{ total() }}</strong>`,
})
export class OrderTotal {
  readonly total = input.required<number>();
}

// Opting back into the always-check behaviour.
@Component({
  selector: 'app-legacy-order-panel',
  changeDetection: ChangeDetectionStrategy.Eager,
  template: `<p>{{ summary }}</p>`,
})
export class LegacyOrderPanel {
  summary = '';
}

go deeper

for a junior

Recall the two names, that OnPush is the default since Angular 22, and the one-line rule: Eager is refreshed on every pass, OnPush only after something marks it.

for a middle

List the four things that mark an OnPush view and explain that input comparison is by identity, so mutation is invisible.

for a senior

Explain what the strategy does not do: it starts no pass, cannot rescue an Eager child under a skipped OnPush parent, and does not cover projected content.

for a principal

Argue why OnPush as the default is a design-contract choice: it forces explicit update triggers, and weigh when leaving legacy components on Eager is cheaper than rewriting them.

## What change detection means in Angular Angular keeps the DOM in sync with component state by running **change detection passes**. A pass walks the tree of **views** (each component has one, plus embedded views for `@if`, `@for` and similar blocks) from the root down and, for each view it decides to **refresh**, re-evaluates the template's bindings and writes any value that changed to the DOM. The **change detection strategy**, set per component through the `changeDetection` option of `@Component`, decides whether a given component's view is refreshed when a pass reaches it. ## The two strategies | | `ChangeDetectionStrategy.Eager` | `ChangeDetectionStrategy.OnPush` | |---|---|---| | Internal mode | `CheckAlways` | `CheckOnce` (on demand) | | Refreshed when a pass reaches it | every time | only when the view is marked dirty or for refresh | | Default | before v22 (then called `Default`) | since v22 | | Cost in a large tree | pays for every binding on every pass | skips views nothing has marked | | Tolerates in-place mutation | yes, the next pass re-reads it | no, nothing marks the view | `Eager` is the old behaviour under a new name. `ChangeDetectionStrategy.Default` still exists, but only as a **deprecated alias** with the same value as `Eager`, flagged as due to be removed. ## What marks an OnPush view An `OnPush` view is refreshed on the next pass when one of these has happened: - a parent template binding passed an input a value that is **not identical** (`Object.is`) to the previous one; - an event bound through Angular (a `(click)` in the template, an output listener, a host listener) fired in the component or one of its descendants; - `ChangeDetectorRef.markForCheck()` was called for it or a descendant; - a **signal read in its template** changed. Anything else, such as mutating an object the component received or assigning a field from a timer callback, leaves the view unmarked and the DOM stale. ## The v22 change 1. Angular 21.2 added `ChangeDetectionStrategy.Eager` as an alias for `Default`. 2. Angular 22.0 made a component whose `changeDetection` is undefined `OnPush` by default, listed as a breaking change. 3. The `ng update` migration for v22 adds `changeDetection: ChangeDetectionStrategy.Eager` to existing components that had no value and rewrites `Default` to `Eager`, so upgraded code keeps its behaviour until someone removes the line. New code is therefore `OnPush` unless it says otherwise, and the current recommendation is to keep it that way. ## What the strategy does not do - **It does not schedule anything.** Neither strategy starts a pass; zone.js or the zoneless scheduler does that in response to notifications. The strategy only decides what a pass refreshes. - **It does not free an Eager child from a skipped parent.** The enum documents that `OnPush` applies to child directives and cannot be overridden: an `Eager` component inside an `OnPush` component that the pass skips is refreshed only when its parent is, or when it is itself marked. - **It does not cover projected content.** Nodes passed in through `<ng-content>` belong to the view of the component that declared them, so they are checked on that parent's schedule, not the receiver's. - **It does not compare deeply.** Input comparison is by identity; a mutated object with the same reference counts as unchanged. ## The same component under both strategies Consider a component that shows an order count and changes it in three ways: - **A `(click)` handler increments a plain field.** Works under both: the bound event marks the view, so even `OnPush` refreshes it. - **A `subscribe()` callback assigns the field.** Under `Eager` in a zone-based app it appears on the next pass, because every pass refreshes the view. Under `OnPush` nothing marks the view, so the old value stays on screen. - **The field becomes a signal and the callback calls `set()`.** Works under both, because the template's signal read marks the view whichever strategy it uses. This is the practical meaning of the two strategies: `Eager` tolerates state that changes behind Angular's back, while `OnPush` requires every change to arrive through a trigger Angular can see. ## Choosing `OnPush` is the right default for new components: it makes a component's update triggers explicit (inputs, bound events, signals) and lets Angular skip whole subtrees. `Eager` remains useful as a temporary setting for legacy components that change template state outside those triggers, until they are rewritten to use signals or immutable updates.

  • If an Eager component sits inside an OnPush parent that nothing has marked, is the Eager child refreshed?
    Not by that pass. When Angular skips the OnPush parent's view, the Eager child is refreshed only if its parent is refreshed or the child itself is marked, for example by a signal its own template reads. The strategy enum documents this as OnPush applying to child directives without an override.
  • Does OnPush stop Angular from checking content projected into the component through <ng-content>?
    No. Projected nodes belong to the view of the component that declared them, so they are checked when that parent is checked. An OnPush wrapper can skip its own template, but the content a parent projects into it follows the parent's schedule.
  • What happens to existing ChangeDetectionStrategy.Default code when a project upgrades to Angular 22?
    It still compiles, because `Default` is a deprecated alias with the same value as `Eager`. The v22 `ng update` migration rewrites `Default` to `Eager` and adds `Eager` to components that had no `changeDetection` value, so upgraded components keep their old behaviour.

saying these in an interview costs you the question

  • ChangeDetectionStrategy.Default is still what a new Angular component gets.
  • OnPush refreshes a component only when one of its inputs changes.
  • Setting OnPush makes Angular start a pass whenever the component's data arrives.
  • An Eager child is always refreshed, even inside a skipped OnPush parent.
  • OnPush deep-compares input objects to decide whether anything changed.