Angular's default change detection model—a full tree traversal on every event—kills performance on mobile and complex applications. OnPush has existed for years, but zoneless Angular (available in v18+) changes the game: it removes the Zone.js layer and lets you explicitly control when to recalculate. Together, these two tools enable predictable, maintainable performance.
Why Default Change Detection is Expensive
Angular uses Zone.js to intercept asynchronous events (setTimeout, promises, DOM events) and automatically trigger change detection. On each detection cycle, Angular traverses the entire component tree, executes template getters, and compares old values to new ones. On an app with hundreds of nested components, this quickly becomes a bottleneck. Worst part: even if only a leaf component changed, the root still re-checks everything for all children.
The problem compounds with frequent network calls, animations, or mobile apps where CPU is constrained. A table with 1,000 rows updating every second? You'll see freezes, jank, and battery drain. Zone.js adds weight too (~36 KB minified) and slows startup.
OnPush: The First Step Toward Control
OnPush tells Angular: "only recalculate this component if its inputs change or something inside explicitly triggers change detection." It's a contract: in return, the component must be immutable—inputs must never mutate, only be replaced.
Concretely, with OnPush, if you have a list component displaying a product table, Angular only re-renders if you replace the entire table or input object. No pointless tree traversal. But it demands discipline: no this.items.push(...) , you must do this.items = [...this.items, newItem] .
@Component({
selector: 'app-product-list',
template: `<div>{{ items | json }}</div>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProductListComponent {
@Input() items: Product[] = [];
}Used correctly, OnPush drastically reduces the number of checks. On a typical app, that's 30–60% faster change detection. But be aware: observables with async pipe, internal events, or direct property mutations can create subtle bugs if the component isn't designed immutably.
Zoneless Angular: Taking Back Control
Zoneless Angular removes Zone.js entirely. No magic interception. Instead, you explicitly declare when an action triggers change detection via ChangeDetectorRef.markForCheck() or ChangeDetectionStrategy.OnPush coupled with signals.
It's a paradigm shift. With zoneless, every event you listen to explicitly in the template or via a listener becomes responsible for signaling a change. Signals (Angular's other major innovation) integrate naturally: when a signal updates, components reading it are automatically marked for re-render.
@Component({
selector: 'app-counter',
template: `<button (click)="increment()">Count: {{ count() }}</button>`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent {
count = signal(0);
increment() {
this.count.update(v => v + 1);
}
}Immediate gains: no Zone.js means smaller bundle, faster startup, and above all, predictable performance. You know exactly when change detection runs. No surprises. On a heavy app with lots of events, that's 20–40% less CPU time overall.
Combining OnPush and Zoneless: The Winning Formula
When you use zoneless with OnPush and signals, you get fine-grained synchronization: each component re-renders only if its direct data changes. No global tree traversal. No expensive interception.
Concrete example: a real-time dashboard with live charts. Without zoneless, each new data point (from a WebSocket) triggers a global check. With zoneless + OnPush, only the chart component updates, and only if it listens to the corresponding signal. Other panels don't move.
Migrating to zoneless takes work: you must audit where you relied on Zone.js's automatic interception. Timers, unawaited promises, third-party calls that don't fire explicit DOM events—all must be refactored. But it's doable gradually. Angular 18+ lets you mix zoneless and Zone.js apps on the same platform.
Common Pitfalls and Solutions
The big pitfall: forgetting that OnPush demands immutability. You do this.user.name = 'John' , and nothing updates, because the object reference didn't change. Solution: use immutable operators or libraries like Immer. With zoneless, it's even more critical—there's no Zone.js safety net to catch your mutation.
Second pitfall: forgetting to inject ChangeDetectorRef when you need it. Some patterns, like forcing a manual check in an async callback, need cdr.markForCheck() . Without zoneless, you could get away with a timeout. With zoneless, it's mandatory.
Third pitfall: mixing signals and observables without thinking about lifecycle. An observable emitting after component destruction can cause memory leaks. Use takeUntilDestroyed() systematically.
Practical Recommendations
Start by auditing your components: which ones receive many inputs? Which are immutable? Enable OnPush on those first. Measure with Lighthouse or Chrome DevTools (Performance tab). A well-optimized app will see tangible gains in First Contentful Paint and Interaction to Next Paint.
For zoneless, test on a branch or feature flag. The raw performance benefits are real, but there is a learning curve. Document your patterns: how do you handle async events? How do you trigger change detection manually?
Don't overlook signals. They're the glue that makes zoneless truly effective. Progressively replace BehaviorSubjects with signals, especially for component-local state. Signals are lighter, faster, and perfectly integrated into the zoneless model.
Conclusion
OnPush and zoneless aren't cosmetic optimizations. They're architectural shifts that redefine how Angular handles reactivity. OnPush forces you to think immutability and reduce change detection cost. Zoneless frees you from Zone.js overhead and gives you full control.
For a modern, fast, and maintainable app, the combination is essential. You won't build a truly performant mobile app without mastering both concepts. Start now: enable OnPush on your leaf components, adopt signals for local state, and explore zoneless for your next critical feature.