Angular just changed everything about how reactivity works.
If you’re still using Zone.js and ChangeDetectionStrategy.OnPush to optimize performance — there’s a better way in 2026.
Angular Signals + Zoneless Change Detection is the biggest performance upgrade Angular has ever shipped.
Here’s everything you need to know.
What Are Angular Signals?
A Signal is a reactive value that automatically notifies Angular when it changes — without Zone.js watching everything.
import { signal, computed, effect } from '@angular/core';
// Create a signal
const count = signal(0);
// Read it
console.log(count()); // 0
// Update it
count.set(1);
count.update(val => val + 1);
// Computed signal — updates automatically
const doubled = computed(() => count() * 2);
// Effect — runs when signals change
effect(() => {
console.log('Count changed:', count());
});
Simple. Powerful. Zero boilerplate.
The Problem With Zone.js
Before Signals, Angular used Zone.js to detect changes. Every time anything happened — a click, an HTTP request, a setTimeout — Zone.js triggered change detection across your entire app.
This is why Angular apps got slow at scale.
// Old way — Zone.js watches everything
@Component({
changeDetection: ChangeDetectionStrategy.OnPush // needed for performance
})
export class OldComponent {
count = 0;
increment() {
this.count++; // Zone.js detects this
}
}
The problem? Zone.js can’t know which component actually changed. So it checks everything.
The New Way — Signals + Zoneless
// New way — Signals tell Angular exactly what changed
@Component({
template: `
<p>Count: {{ count() }}</p>
<p>Doubled: {{ doubled() }}</p>
<button (click)="increment()">+1</button>
`
})
export class NewComponent {
count = signal(0);
doubled = computed(() => this.count() * 2);
increment() {
this.count.update(v => v + 1);
}
}
Angular now knows exactly which component needs to re-render. No more checking everything.
How to Enable Zoneless Change Detection
In Angular 21, you can go fully zoneless:
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideExperimentalZonelessChangeDetection } from '@angular/core';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, {
providers: [
provideExperimentalZonelessChangeDetection()
]
});
Then remove Zone.js from your polyfills:
// angular.json — remove this line
"polyfills": ["zone.js"] // ❌ Remove
// After
"polyfills": [] // ✅ Clean
Real Performance Difference
Here’s what you gain going zoneless with Signals:
| Metric | Zone.js | Signals + Zoneless |
|---|---|---|
| Change detection scope | Entire app | Only changed components |
| Bundle size | +33KB (zone.js) | 0KB overhead |
| Initial render | Slower | Up to 45% faster |
| Memory usage | Higher | Lower |
| Large list performance | Degrades | Stays fast |
Signal Inputs — The New @Input()
Angular 21 replaces @Input() with signal inputs:
// Old way
@Component({})
export class CardComponent {
@Input() title: string = '';
@Input() count: number = 0;
}
// New way — Signal inputs
@Component({
template: `
<h2>{{ title() }}</h2>
<p>{{ count() }}</p>
`
})
export class CardComponent {
title = input<string>('');
count = input<number>(0);
// Computed from inputs
summary = computed(() =>
`${this.title()} has ${this.count()} items`
);
}
Signal inputs are readonly — no accidental mutations. And they work perfectly with computed signals.
Signal Outputs — The New @Output()
// Old way
@Component({})
export class ButtonComponent {
@Output() clicked = new EventEmitter<void>();
}
// New way
@Component({
template: `<button (click)="handleClick()">Click me</button>`
})
export class ButtonComponent {
clicked = output<void>();
handleClick() {
this.clicked.emit();
}
}
Model Signals — Two-Way Binding
Angular 21 introduces model() for two-way binding:
@Component({
template: `
<input [value]="name()" (input)="name.set($event.target.value)" />
<p>Hello, {{ name() }}!</p>
`
})
export class FormComponent {
name = model('');
}
Migration Strategy
Don’t rewrite everything at once. Follow this order:
Step 1 — Start using signal() for local component state
Step 2 — Replace @Input() with input()
Step 3 — Replace @Output() with output()
Step 4 — Replace services with signalStore() (NgRx Signals)
Step 5 — Enable zoneless change detection
Each step is independent — you can migrate gradually. ✅
Common Mistakes to Avoid
❌ Reading signals outside reactive context:
// Wrong — won't track changes
ngOnInit() {
const value = this.count(); // reads once, not reactive
}
// Right — use effect() for side effects
effect(() => {
console.log(this.count()); // tracks changes
});
❌ Mutating signal values directly:
// Wrong
const items = signal([1, 2, 3]);
items().push(4); // doesn't trigger update!
// Right
items.update(arr => [...arr, 4]);
❌ Creating signals inside loops or conditions:
// Wrong
if (someCondition) {
const signal = signal(0); // unstable
}
// Right — always at component level
mySignal = signal(0);
Want a Production-Ready Angular 21 Setup?
I built NgMFE Starter Kit — a complete Angular 21 Micro-Frontend boilerplate with Signals and NgRx Signals pre-configured:
- ⚡ Angular 21 + Native Federation
- 📡 NgRx Signals for state management
- 🔐 JWT Auth + Route Guards
- 🌍 Arabic RTL support
- 🌙 Dark/Light mode
- 🚀 CI/CD with GitHub Actions + Vercel
- ✅ 13 unit tests passing
🔴 Live Demo: https://ng-mfe-shell.vercel.app
(login: admin/admin)
🛒 Get the kit:
👉 https://mhmoudashour.gumroad.com/l/hdditr
💼 Need it set up for your project?
👉 https://www.upwork.com/services/product/development-it-set-up-angular-micro-frontend-architecture-for-your-enterprise-app-2037100004401414520?ref=project_share
Have questions about Angular Signals or the migration? Drop them in the comments! 🙏
Top comments (0)