Jacob Wiseberg

← back

Still Fighting Angular Change Detection

I’ve used Angular on and off for a while, but it took a slow, frustrating weekend tracking down an unnecessary re-render to make me actually sit down and understand change detection instead of just working around it.

The symptom

I was building a small dashboard for myself, nothing exotic, and noticed a component was re-rendering constantly even when nothing it displayed had changed. The browser’s performance profiler showed dozens of change detection cycles a second on a page that should have been almost entirely static between user actions.

What I’d been misunderstanding

My mental model of Angular’s default change detection was fuzzy at best: “it checks things when they change.” The reality is blunter. By default, Angular runs change detection on the entire component tree whenever practically anything happens: a click anywhere, a timer firing, an HTTP response resolving, any event Zone.js knows how to patch. It doesn’t know what actually changed; it just re-checks everything and lets Angular’s diffing sort it out.

That’s fine for small apps and actively expensive once you have enough components, especially ones doing non-trivial work in their templates.

The fix that actually mattered

Switching the heavy component to OnPush was the single biggest win:

@Component({
  selector: 'app-dashboard-panel',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `...`
})
export class DashboardPanelComponent {
  @Input() data: PanelData;
}

OnPush tells Angular: only re-check this component when its @Input() references change, a DOM event inside it fires, or you explicitly ask. The catch, which cost me an hour of confusion, is that it checks for reference changes, not deep equality. Mutating an object in place and expecting OnPush to notice is a guaranteed bug:

// This does nothing under OnPush — same reference, no change detected
this.data.count++;

// This works — new reference
this.data = { ...this.data, count: this.data.count + 1 };

Once I internalized that distinction, a surprising number of “Angular isn’t updating” bugs I’d shrugged off over the years retroactively made sense.

Where template bindings bit me

I also found a function call sitting directly in a template binding:

<div>{{ calculateTotal(items) }}</div>

That function runs on every single change detection cycle, dozens of times a second in my case, recalculating a sum that only actually needed to change when items changed. Moving it to a cached value updated only when items actually changes, or to a pure pipe, cut the wasted work entirely:

@Pipe({ name: 'total', pure: true })
export class TotalPipe implements PipeTransform {
  transform(items: Item[]): number {
    return items.reduce((sum, i) => sum + i.value, 0);
  }
}

A pure pipe only recomputes when its input reference changes, which is exactly the behavior I’d been assuming the template expression already had for free.

What I’d tell past me

Change detection isn’t magic and it isn’t free; it’s a blunt tool that trades correctness-by-default for performance you have to opt into. OnPush plus immutable data patterns plus keeping logic out of template expressions solved every case I ran into. None of it is exotic once you know to look for it, which is exactly why it took a frustrating weekend to actually learn instead of a five-minute read.