Every frontend framework has to solve the same foundational challenge: when data changes in memory, how does the framework know which pixels on the screen need updating?
For nearly ten years, Angular had the strangest answer in modern web development: it hijacked your entire browser.
If you called setTimeout, Angular intercepted it. If you clicked a button, Angular hijacked addEventListener. If you fetched data over the network, Angular wrapped fetch and XMLHttpRequest.
The library doing this heavy lifting was called Zone.js. It did not actually know what data changed, or which component owned that data. It only knew one blunt fact: an asynchronous browser API had finished running.
Because something might have changed, Angular did the only thing it could: it walked down your entire component tree, checking every single binding from top to bottom.
Over the last few versions, the Angular team quietly pulled off one of the most audacious architectural rewiring jobs in frontend history. They removed the monkey-patches, dismantled the global tree scans, and rebuilt Angular around Signals and Zoneless change detection.
To write modern, high-performance Angular, you have to understand how the old trick worked, why it ran out of road, and how the new reactive engine actually updates your DOM today.
To understand where Angular is today, we have to look at what Zone.js was actually doing behind the scenes.
Notice what is missing here: No setState() call like in React. No this.triggerUpdate() call. Just a plain property mutation: this.count++.
Angular had no idea that count was touched. Instead, when your application booted up, Zone.js replaced the browser's native addEventListener with a patched wrapper.
When the user clicked the button, the execution pipeline went like this: The browser fired the native click event. Zone.js intercepted the callback. Your increment() function ran and mutated this.count. Zone.js detected that the microtask queue was empty (onMicrotaskEmpty). Zone.js notified Angular: "An async task just completed." Angular executed ApplicationRef.tick(). Angular started at the very top of your application (the Root Component) and traversed every active child component, evaluating every template expression to see if any value differed from the previous cycle.
If you had a dashboard with 2,000 components and a setInterval ticking in a background widget, Angular could end up checking thousands of template expressions every second, even if 99% of those components were completely static.
To stop these wasteful global tree traversals, Angular introduced ChangeDetectionStrategy.OnPush.
When you marked a component as OnPush, you gave Angular permission to skip that component's subtree during a change detection cycle, unless one of a few specific conditions was met: One of the component's @Input() references changed to a new memory address. An event listener declared directly inside that component's template was triggered. An AsyncPipe in the template emitted a new value. You manually injected ChangeDetectorRef and called markForCheck().
OnPush delivered enormous performance boosts, but it shifted mental overhead onto the developer. Mutating an object property in place broke change detection because the object reference remained unchanged.
Developers spent years debugging "ghost states", situations where component properties held fresh data, but the screen refused to re-render because no input reference had swapped.
Modern Angular replaces the global guessing game with a fine-grained reactive dependency graph powered by Signals.
A Signal is not an event stream, and it is not an RxJS Subject. A Signal is a synchronous reactive value wrapper that implements a two-way contract between Producers and Consumers.
