Exploring Angular's Change Detection: In-Depth Analysis
Published: 2024-07-07 22:18:19

## Understanding Change Detection

Change detection in Angular is a key process that keeps the app's state and user interface in sync. It's how Angular makes sure our UI updates when the underlying data changes. Without it, any changes in your app wouldn't show up in the UI automatically, making the app inconsistent and unreliable. Change detection is important because it keeps the UI accurate, ensuring every user action, data fetch, or event is shown correctly, making the app responsive and user-friendly.


## How Change Detection Works in Angular

The change detection process in Angular involves two main stages:

1. **Marking the Component as Dirty:** This initial stage occurs when an event that can alter the state of a component happens. For instance, a user clicks a button, which triggers Angular to mark the affected component as `dirty`. This marking indicates that the component requires a check-up for potential changes.

2. **Refreshing the View:** This is where `zone.js` comes into play. zone.js is a library that helps Angular track asynchronous operations like XHR requests, DOM events, and timers (e.g., `setInterval`, `setTimeout`). When an asynchronous event occurs, Angular captures it through the [onMicrotaskEmpty](https://github.com/angular/angular/blob/main/packages/core/src/change_detection/scheduling/ng_zone_scheduling.ts#L47-L60) Observable:

```typescript
this._onMicrotaskEmptySubscription = this.zone.onMicrotaskEmpty.subscribe({
  next: () => {
    if (this.changeDetectionScheduler.runningTick) {
      return;
    }
    this.zone.run(() => {
      this.applicationRef.tick();
    });
  },
});
```

And [traverse the view tree](https://github.com/angular/angular/blob/18.0.0-rc.1/packages/core/src/application/application_ref.ts#L585C9-L592C10) to detect and propagate any changes:

```typescript
for (let {_lView, notifyErrorHandler} of this._views) {
  detectChangesInViewIfRequired(
    _lView,
    notifyErrorHandler,
    isFirstPass,
    this.zonelessEnabled,
  );
}
```

In Angular, a `view` refers to an instance of [ViewRef](https://github.com/angular/angular/blob/main/packages/core/src/render3/view_ref.ts#L45). Think of `ViewRef` as a box that contains a bunch of important information about the component, such as the current state of inputs, the template, directives being used, and binding statuses. Each component has its own `ViewRef` box, making it easier for Angular to manage and update components during change detection.

The `detectChangesInViewIfRequired` method triggers the [detectChangesInView](https://github.com/angular/angular/blob/18.0.0-rc.1/packages/core/src/render3/instructions/change_detection.ts#L450), which defines the criteria for checking components, inspecting various flags associated with the view and its mode, to determine if any updates are necessary.

If the view needs refreshing, the [refreshView](https://github.com/angular/angular/blob/18.0.0-rc.1/packages/core/src/render3/instructions/change_detection.ts#L489C5-L489C16) method is called. This method executes the [template function](https://github.com/angular/angular/blob/18.0.0-rc.1/packages/core/src/render3/instructions/shared.ts#L431) (which is a component template compiled into a regular JavaScript function) with the render flags and context to generate the component view and starts a chain reaction by triggering event detection for the view's child components through the [detectChangesInChildComponents](https://github.com/angular/angular/blob/18.0.0-rc.1/packages/core/src/render3/instructions/change_detection.ts#L281C7-L281C37) method.

So to perform change detection, Angular traverses the views' tree and executes template functions on each component.


## Default Change Detection

In the beginning, most of us used Default change detection - it's like being in the middle of a bustling city – there's noise everywhere, distractions pulling your attention in every direction. This is how it was with Angular's default change detection. It keeps an eye on everything, even when nothing's happening. And sure, it works, but it's a bit overkill, causing performance hiccups, especially as your app grows.

This strategy is the default change detection mechanism, and it’s applied automatically unless we explicitly override it with an OnPush strategy. It relies on the [CheckAlways](https://github.com/angular/angular/blob/18.0.0-rc.1/packages/core/src/render3/instructions/change_detection.ts#L458) flag which means that Angular will run change detection for all components whenever any event occurs.

```typescript
let shouldRefreshView: boolean = !!(
  mode === ChangeDetectionMode.Global && flags & LViewFlags.CheckAlways
);
```

So if this flag is set, then it calls the mentioned `refreshView` method (`shouldRefreshView` flag is set to `true`), 

```typescript
if (shouldRefreshView) {
  refreshView(tView, lView, tView.template, lView[CONTEXT]);
}
```

which refreshes not just the current view but also all its child views, if there are any. This ensures everything stays consistent, but it can be inefficient for larger apps because it might do more checks than needed.


![Click on Component D, which triggers an animation throughout the entire DOM tree from top bo the bottom](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/68fmdqi4mfyfs8t204ul.gif)

So, it's kind of like a domino effect, making sure the entire component tree gets updated, whatever happens.

We have the ability to control this one way or another. We can explicitly detach and reattach the view from the change detection tree using [detach()](https://github.com/angular/angular/blob/main/packages/core/src/render3/view_ref.ts#L220-L222) and [reattach()](https://github.com/angular/angular/blob/main/packages/core/src/render3/view_ref.ts#L280-L283) methods respectively. 

```typescript
@Component({
 selector: 'detached', template: `Detached Component`
})
export class DetachedComponent {
 constructor(private cdr: ChangeDetectorRef) {
   cdr.detach();
 }
}
```

If we detach the view from the change detection, Angular will skip it, regardles of any changes chappend.


## OnPush Change Detection Strategy

For better performance, Angular offers the `OnPush` change detection strategy. This strategy tells Angular to skip change detection for the component unless one of its inputs changes, we mark component to check using `markForCheck` method or an event occurs, for example a XHR request handled by an `async` pipe.

By focusing on what's changed, rather than checking everything all the time, `OnPush` makes apps faster and more efficient, reduces unnecessary updates, improves performance, and prevents potential disasters.

Using `OnPush` helps optimize performance by reducing the number of times change detection runs, especially in large and complex applications.

So, how does it work? I mentioned that `OnPush` change detection gets triggered when we do things like setting a new Input value in a child component.

Now, let's get back to the Angular source code, and verify the part that runs when we [set a new Input value](https://github.com/angular/angular/blob/main/packages/core/src/render3/component_ref.ts#L442-L469).

So, when the `setInput` method is called, it confirms that the Input value actually has been changed,

```typescript
if (
  this.previousInputValues.has(name) &&
  Object.is(this.previousInputValues.get(name), value)
) {
  return;
}
```

marking the component view as `Dirty` in the [markViewDirty](https://github.com/angular/angular/blob/main/packages/core/src/render3/component_ref.ts#L460)  method, which basically tells Angular, 

> "Hey, this child component's view needs updating, so take a look next time you check for changes."


The same idea applies when, for example, we are using an [Async pipe](https://github.com/angular/angular/blob/18.0.0-rc.1/packages/common/src/pipes/async_pipe.ts#L102
).



It subscribes to observables, which updates the latest value by [calling the markForCheck function](https://github.com/angular/angular/blob/18.0.0-rc.1/packages/common/src/pipes/async_pipe.ts#L194), 

```typescript
private _updateLatestValue(async: any, value: Object): void {
  if (async === this._obj) {
    this._latestValue = value;
    if (this.markForCheckOnValueUpdate) {
      this._ref?.markForCheck();
    }
  }
}
```

which, in turn, triggers the [markViewDirty](https://github.com/angular/angular/blob/main/packages/core/src/render3/view_ref.ts#L164) method.


`markForCheck` is also used in a few other scenarios. For example if we want to initiate the change detection manually, we handle the DOM event, or attaching, detaching a view. And in all these scenarios at some point we call the [markViewDirty](https://github.com/angular/angular/blob/main/packages/core/src/render3/instructions/mark_view_dirty.ts#L41-L50) method.

```typescript
while (lView) {
  lView[FLAGS] |= dirtyBitsToUse;
  const parent = getLViewParent(lView);

  if (isRootView(lView) && !parent) {
    return lView;
  }
 
  lView = parent!;
}
```

This method is like a domino effect –  it starts with the specified view and moves up the family tree, by [checking if there’s a parent](https://github.com/angular/angular/blob/main/packages/core/src/render3/instructions/mark_view_dirty.ts#L45), 

```typescript
if (isRootView(lView) && !parent)
```

marking each view along the path as [Dirty](https://github.com/angular/angular/blob/main/packages/core/src/render3/instructions/mark_view_dirty.ts#L42), meaning they all need checking.

```typescript
const dirtyBitsToUse = isRefreshingViews() ? LViewFlags.Dirty : LViewFlags.RefreshView | LViewFlags.Dirty;

while (lView) {
 lView[FLAGS] |= dirtyBitsToUse;
 …
}
```

And when it's time to do the actual checking, Angular [looks at those views marked as dirty](https://github.com/angular/angular/blob/main/packages/core/src/render3/instructions/change_detection.ts#L475-L479).

```typescript
shouldRefreshView ||= !!(
  flags & LViewFlags.Dirty &&
  mode === ChangeDetectionMode.Global &&
  !isInCheckNoChangesPass
);
```


If a component's view is marked as `Dirty`, it means something in that component has changed, and it needs to be re-rendered. And we do that by calling the [refreshView](https://github.com/angular/angular/blob/main/packages/core/src/render3/instructions/change_detection.ts#L497) method.


![Click on Component D triggers an animation on the clicked element and all its ancestors, starting from the root component](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zh36pb3hlvr8hobv25pq.gif)

When we click on the component, it triggers a change detection in the component itself and all its parent components, which is exactly what we should expect, because we marked all its ancestors as `Dirty` in the `markViewDirty` method.

## Signals Era

Since version 16, Angular has been introducing Signals - a wrapper around a value that can notify interested consumers when that value changes. It can contain any value, from simple primitives to complex data structures, and we can read the value through a getter function, which allows Angular to track where the signal is used.

Now, the cool part is, Angular allows us to utilize this Signal power and combine it with the `OnPush` strategy to mark certain components for updates.

This little trick eliminates the need for unnecessary checks on components, whether they're parent or child elements.

Let's see what makes the signals work the way they do. Whenever we tweak a signal within a component's template, using methods like `set()` or `update()`, it's like setting off a little chain reaction. 

First, Angular calls [signalSetFn](https://github.com/angular/angular/blob/229dd8355e91388f97523fc085197cf06d2fda65/packages/core/primitives/signals/src/signal.ts#L71), which will [send out a notification](https://github.com/angular/angular/blob/229dd8355e91388f97523fc085197cf06d2fda65/packages/core/primitives/signals/src/signal.ts#L108) to the live consumer waiting in the view, 

```typescript
function signalValueChanged<T>(node: SignalNode<T>): void {
  …
  producerNotifyConsumers(node);
  …
}
```

making it go, ["Hey, I'm dirty!"](https://github.com/angular/angular/blob/main/packages/core/primitives/signals/src/graph.ts#L304C9-L304C26) 


```typescript
for (const consumer of node.liveConsumerNode) {
  if (!consumer.dirty) {
    consumerMarkDirty(consumer);
  }
}
```

Then, it [marks all its ancestors, right up to the root, with a flag called HasChildViewsToRefresh](https://github.com/angular/angular/blob/main/packages/core/src/render3/util/view_utils.ts#L262), indicating 

> "Hey, I've got some child views here that need to be refreshed."

```typescript
while (parent !== null) {
  if (parent[FLAGS] & LViewFlags.HasChildViewsToRefresh) {
    break;
  }

  parent[FLAGS] |= LViewFlags.HasChildViewsToRefresh;
  if (!viewAttachedToChangeDetector(parent)) {
    break;
  }
  parent = getLViewParent(parent);
}
```

If we take a look closer, we'll see that this method is pretty similar to the `markViewDirty` method. The difference is, instead of marking all parent views with the `Dirty` flag, we mark them with the `HasChildViewToRefresh` flag.

Now, as you already know, the change detection mechanism consists of two parts and the subsequent part traverses the tree of views. So let's back into our `detectChangesInView` method that evaluates whether a view requires updating.

Now, here's the clever part - when a view is [marked with this HasChildViewsToRefresh flag](https://github.com/angular/angular/blob/main/packages/core/src/render3/instructions/change_detection.ts#L498-L504), there's no need to re-render it. Angular skips right ahead to checking out the child component view if there's any. 

```typescript
else if (flags & LViewFlags.HasChildViewsToRefresh) {
  detectChangesInEmbeddedViews(lView, ChangeDetectionMode.Targeted);
  const components = tView.components;
  if (components !== null) {
    detectChangesInChildComponents(lView, components, ChangeDetectionMode.Targeted);
  }
}
```

This smart shortcut helps Angular avoid wasting time on unnecessary stuff, ensuring smooth and efficient operation!

When we use e.g. the `setInterval` function to update the signal value in `Component D`, only the components directly touched by the signal change actually get refreshed.

![Click on Component D, which triggers an animation only on itself](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/awent00qlx3wc54z0vpn.gif)

`OnPush` within Signals is like a sniper shot for detecting changes. It allows us to specify which components should be re-rendered when certain signals change. This means that instead of refreshing the entire component tree or a single branch when using `OnPush`, we only re-render the components directly affected by the signal change.


## Wrap up 

Default change detection relies on the `CheckAlways` flag, triggering updates for the entire view tree, whatever happens. This keeps everything consistent but can be overkill, causing unnecessary performance hits as the app grows.

`OnPush` change detection is a powerful optimization technique in Angular. It ensures that components are only re-rendered when their inputs change, when events occur within the component or when we manually trigger change detection using the `markForCheck` method. It's important to note that when we use `OnPush`, the change detection refreshes not only the component itself but also all its parent components in the component tree.

`OnPush` within Signals is a more targeted approach to change detection. It allows us to specify which components should be re-rendered when certain signals change. This means that instead of refreshing the entire component tree or a single branch when using `OnPush`, we only re-render the components directly affected by the signal change.