
mhmoud ashourAngular just changed everything about how reactivity works. If you’re still using Zone.js and...
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.
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.
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.
// 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.
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
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 |
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.
// 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();
}
}
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('');
}
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. ✅
❌ 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);
I built NgMFE Starter Kit — a complete Angular 21 Micro-Frontend boilerplate with Signals and NgRx Signals pre-configured:
🔴 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! 🙏