Skip to content
    All posts
    Angular

    Angular Change Detection: OnPush, Zoneless, and What Actually Makes an App Fast

    Most Angular performance problems aren't change detection. But when they are, the fix is rarely the one people reach for first.

    3 min readby

    A client's payroll grid took 1.8 seconds to respond to a keystroke. The first suggestion from the team was OnPush everywhere. We tried it. It got faster — to 1.5 seconds. The actual problem was a method call in the template that recomputed a currency total for every one of 900 rows, on every check. OnPush reduced how often that happened. It didn't make it cheap.

    That's the pattern I keep seeing. Change detection strategy is a multiplier on the cost of your bindings. If the bindings are expensive, you're optimising the wrong term.

    Rule 1: never call a function from a template

    html
    <!-- runs on every single check, for every row -->
    <td>{{ calculateNetPay(row) }}</td>
    
    <!-- computed once per data change -->
    <td>{{ row.netPay }}</td>

    Precompute in the component, or use a computed signal, or use a pure pipe. A pure pipe is memoised on its inputs; a method call is not memoised on anything. This single rule has fixed more Angular performance complaints for me than every other technique combined.

    Rule 2: `track` in `@for` is not optional

    The new control flow requires track, which is one of the better decisions the Angular team has made — the old trackBy was optional and therefore routinely omitted. Without identity tracking, changing one row destroys and rebuilds every DOM node in the list. With it, one row updates.

    html
    @for (employee of employees(); track employee.id) {
      <app-employee-row [employee]="employee" />
    } @empty {
      <app-empty-state message="No employees match these filters." />
    }

    Track by a stable identity, not by index. Tracking by index means the identity changes whenever the list is sorted or filtered, which defeats the purpose entirely.

    Rule 3: OnPush is a contract, not a switch

    OnPush says: only re-check me when an input reference changes, an event fires inside me, or an async pipe emits. It is fast because it does less work. It is also a promise that your component's inputs are immutable — and if you break that promise by mutating an object in place, you get a component that silently shows stale data.

    Signals change this calculus significantly. A signal read in a template marks that specific view dirty when it changes, which means OnPush correctness stops depending on you being disciplined about references. If you are adopting signals, adopt OnPush alongside them — together they're coherent in a way neither is alone.

    Zoneless: worth it, with a caveat

    Zone.js patches every async API in the browser to know when to check for changes. It works, and it costs you: a payload on every page load, and change detection runs triggered by things like a setTimeout in a third-party analytics script.

    Going zoneless with provideZonelessChangeDetection() removes that entirely. Change detection then runs only when something explicitly signals a change — a signal write, an async pipe emission, or markForCheck().

    The caveat: any code that updates state outside those mechanisms stops updating the UI. In practice this means third-party libraries with imperative callbacks — chart libraries, map SDKs, older jQuery-era widgets. Audit those before you flip the flag, and wrap their callbacks so they write to a signal.

    The measurement step people skip

    Before changing anything, open Angular DevTools, hit the profiler, and interact with the slow screen. It shows you which components are being checked and how long each takes. Twice now this has told me the slow component wasn't the one anyone suspected — once it was a tooltip directive doing a layout read in a binding.

    1. 01Profile first, and write down the number.
    2. 02Fix the expensive bindings — template method calls, missing track, unbounded lists.
    3. 03Then apply OnPush (or signals) to reduce how often the remaining work happens.
    4. 04Consider zoneless once the app is signal-based.
    5. 05Profile again and compare to the number you wrote down.

    Steps 1 and 5 are the ones that get skipped, and they're the only two that tell you whether the other three helped.

    AngularPerformanceOnPushZoneless

    Keep reading

    PAPulapa Arun Kumar

    Full-stack developer building performant, clean, and user-friendly web & mobile applications. Also written as Arun Kumar Pulapa — same person, surname first.

    Built with

    ReactTypeScriptTailwind CSSVite

    © 2026 Pulapa Arun Kumar. All rights reserved.