Performance & Forms
1️⃣ Change Detection Basics (What ACTUALLY happens)
Angular checks bindings → template expressions → pipes → functions
This happens when:
Any event (click, input)
setTimeout,PromiseHTTP response
Observable emits
Parent component updates
❌ Performance killer
<div>{{ getTotal() }}</div>
getTotal() {
return this.items.reduce((a, b) => a + b.price, 0);
}
👎 Runs on every change detection, even mouse move.
✅ Fix (Derived value)
total = computed(() =>
this.items().reduce((a, b) => a + b.price, 0)
);
computed() is NOT executed on every change detection.
it is:
Reactive
Memoized
Dependency-tracked
Runs once
Cached value
Re-runs ONLY when items changes
📌 Rule:
Templates must be dumb
No logic, no functions.
2️⃣ OnPush Strategy (Stop unnecessary checks)
Default behavior (what Angular does normally)
Parent changes → ALL children checked
Angular keeps checking everything again and again.
That’s why big apps feel slow.
Parent change
└─ Child A checked
└─ Child B checked
└─ Child C checked
└─ Grandchildren checked
OnPush behavior (the “smart” mode)
Angular says:
“I will NOT check this component again unless I have a GOOD reason.”
- OnPush Strategy - Checked me ONLY IF:
@Input() reference changes this.user = { ...this.user, name: 'Kamal' }; // ✅ new objec
- Event inside component
increment() {
this.count++;
}
- Observable emits (
async)
- Signal changes
count = signal(0);
increment() {
this.count.update(v => v + 1);
}
<p>{{ count() }}</p>
- When should YOU use OnPush?
- ✅ Container components
- ✅ Table-heavy UIs
- ✅ Dashboard screens
- ✅ Anything using Observables / Signals
✅ Use OnPush ALWAYS unless proven otherwise
@Component({
changeDetection: ChangeDetectionStrategy.OnPush
})
export class OrderRowComponent {}
❌ Common OnPush bug
this.orders.push(newOrder); // ❌ reference unchanged
✅ Correct
this.orders = [...this.orders, newOrder];
📌 Immutability = OnPush works
3️⃣ trackBy (Fix DOM thrashing)
Without trackBy, Angular:
Angular thinks like this 👇
“Hmm… array changed.
I DON’T KNOW which item is which.
Safer to destroy everything and rebuild.”Destroys ALL rows
Recreates ALL rows
❌ Bad
<tr *ngFor="let o of orders">
✅ Good
<tr *ngFor="let o of orders; trackBy: trackById">
trackById(_: number, o: Order) {
return o.id;
}
- Now Angular thinks:
“Ah! Each row has an identity (id).
Let me match old DOM with new data.”
Old DOM: Row(id=1) Row(id=2)
New data: Row(id=99) Row(id=1) Row(id=2) 📌 Result:
DOM reused
Scroll position preserved
Inputs don’t reset
✅ Only ONE new row created
-
✅ Others reused
-
✅ Fast
-
✅ No flicker
4️⃣ Reactive Forms Architecture (How pros design forms)
❌ Anti-pattern (God Form)
this.form = this.fb.group({
name: [],
email: [],
address: [],
payment: [],
shipping: []
});And in the SAME component:
-
Builds the form
-
Renders HTML
-
Handles submit
-
Calls API
-
Does validation
-
Shows errors
-
Manages loading
Why this is BAD (real reasons)
❌ File becomes 600–1000 lines
❌ Impossible to reuse UI
❌ Unit tests are painful
❌ One tiny UI change breaks logic
❌ You can’t split the form later
This is called a God Component.
Hard to validate, test, and reuse.
✅ Correct Architecture
Container (smart)
Responsibilities
-
Creates the
FormGroup -
Owns business rules
-
Handles submit
-
Talks to API
-
Decides what happens
-
export class UserFormContainerComponent {
form = this.fb.group({
name: ['', Validators.required],
email: ['', Validators.email],
address: this.fb.group({
city: [''],
zip: ['']
})
});
submit() {
if (this.form.valid) {
this.api.save(this.form.value);
}
}
}
This component does NOT care about HTML details.
Presentational (dumb)
Receives
FormGroupEmits UI events
@Component({ ... })
export class UserFormComponent {
@Input() form!: FormGroup;
@Output() submitForm = new EventEmitter<void>();
}
<form [formGroup]="form" (ngSubmit)="submitForm.emit()">
<input formControlName="name">
<input formControlName="email">
<button type="submit">Save</button>
</form>📌 This component:
❌ Does NOT create the form
❌ Does NOT call API
❌ Does NOT know backend exists
📌 Same rule as components → separation of concerns
🔁 How data flows (THIS IS THE KEY)
Container
↓ FormGroup (state)
Presentational
↓ User types
FormGroup updates
↓ Submit event
Container handles logic
5️⃣ Custom Validators (Real-world)
Field-level validator
function strongPassword(control: AbstractControl) {
return control.value?.length >= 8 ? null : { weak: true };
}
password: ['', strongPassword]
6️⃣ Cross-field Validators (Very important)
Example: password & confirm password
function passwordMatch(group: AbstractControl) {
const p = group.get('password')?.value;
const c = group.get('confirm')?.value;
return p === c ? null : { mismatch: true };
}
this.form = this.fb.group(
{
password: [''],
confirm: ['']
},
{ validators: passwordMatch }
);
📌 Cross-field = group validator, not control validator
7️⃣ Performance Debugging (No guessing anymore)
Tools you MUST use
Angular DevTools
Change Detection Profiler
Chrome Performance tab
Debug checklist
Ask:
Is this component
OnPush?Are functions called in template?
Is
trackBymissing?Are objects mutated?
Is form recalculating on every keystroke?
Mental Model (Remember this)
❌ Slow Angular = too many checks + DOM recreation
✅ Fast Angular = OnPush + immutable data + derived values
✔ “This list is slow because no trackBy”
✔ “This validator belongs to group, not control”
✔ “This performance fix is correct — not a guess”
Advanced Angular Performance & Forms – Extra Techniques 🔥
8️⃣ Kill Change Detection at the Source (Zone tricks)
❌ Problem
Angular runs change detection for:
scroll
mousemove
resize
3rd-party libraries
✅ Solution: NgZone.runOutsideAngular
constructor(private zone: NgZone) {}
ngAfterViewInit() {
this.zone.runOutsideAngular(() => {
window.addEventListener('scroll', () => {
// no change detection triggered
});
});
}
📌 Use ONLY when UI doesn’t need updates
(ex: analytics, infinite scroll listeners)
9️⃣ Detach Change Detection (Rare but deadly effective)
constructor(private cdr: ChangeDetectorRef) {}
ngOnInit() {
this.cdr.detach();
}
Manually reattach:
this.cdr.detectChanges();
📌 Use cases:
Large dashboards
Static reports
Read-only views
⚠️ Use carefully (senior-level tool)
🔟 Signals + OnPush = Near-zero CD
@Component({
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DashboardComponent {
data = signal<Data[]>([]);
}
📌 Signals update only consumers
No tree-wide checking like Observables.
1️⃣2️⃣ Don’t Subscribe in Components (Hidden performance bug)
❌ Bad
this.service.data$.subscribe(v => {
this.data = v;
});Angular does NOT know when to stop checking.
Every time:
-
mouse moves 🖱️
-
key pressed ⌨️
-
timer ticks ⏱️
-
ANY async event
Angular runs change detection
👉 your component gets checked even if data didn’t change
If you forget this 👇
ngOnDestroy() {
this.sub.unsubscribe();
}
The subscription:
-
keeps running
-
keeps memory
-
keeps CPU busy
In large apps → slow app, random bugs, crashes
You’re doing Angular’s job manually
You are:
-
managing lifecycle ❌
-
managing cleanup ❌
-
triggering UI updates ❌
Angular already solved this.
✅ Good - The RIGHT way (what async pipe really does)
data$ = this.service.data$;
<div *ngFor="let d of data$ | async"></div>
You are saying:
“Angular, YOU handle the subscription. I only care about display.”
📌 async pipe:
auto unsubscribe
optimized CD
| Thing | Responsibility |
|---|---|
| Component | Expose streams |
| Template | Render streams |
| async pipe | Subscribe + unsubscribe |
| Angular | Optimize change detection |
1️⃣3️⃣ Smart Debouncing Forms (Critical)
❌ Bad
this.form.valueChanges.subscribe(v => {
this.search(v);
});
✅ Good
this.form.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged()
).subscribe(v => this.search(v));
📌 Reduces API calls by 90%+
1️⃣4️⃣ updateOn – Hidden Form Performance Weapon
this.form = this.fb.group({
email: ['', {
updateOn: 'blur'
}]
});
Options:
'change'(default)'blur''submit'
📌 Perfect for:
Heavy validators
Expensive async validators
1️⃣5️⃣ Async Validators Done Right
❌ Bad
email: ['', null, this.checkEmail]
Triggers on every keystroke.
✅ Good
email: ['', {
asyncValidators: [this.checkEmail],
updateOn: 'blur'
}]
1️⃣6️⃣ Lazy Load Forms & Heavy Components
loadComponent: () =>
import('./big-form.component').then(m => m.BigFormComponent)
📌 Huge win for enterprise apps.
1️⃣7️⃣ Virtual Scroll (Lists > 100 items)
Use cdk-virtual-scroll-viewport
<cdk-virtual-scroll-viewport itemSize="50">
<div *cdkVirtualFor="let item of items">
{{ item.name }}
</div>
</cdk-virtual-scroll-viewport>
📌 Renders only visible rows.
1️⃣8️⃣Avoid Pure Pipe Abuse
❌ Misuse
{{ data | heavyCalculation }}
Runs often.
✅ Better
Precompute
Use computed signal
Or map in stream
1️⃣9️⃣ Split Huge Forms (Very underrated)
Instead of one massive form:
Step forms
Nested
FormGroupLoad sections lazily
📌 Faster validation + easier debugging.
2️⃣0️⃣ Use readonly & disabled correctly
control.disable({ emitEvent: false });
📌 Prevents unnecessary valueChanges emissions.
2️⃣1️⃣ ChangeDetection Debug Trick
Add temporarily:
ngDoCheck() {
console.log('CD ran');
}
📌 You’ll immediately see what causes re-renders
2️⃣2️⃣ Profiling Checklist (Memorize this)
When Angular is slow:
List or form?
OnPush?
trackBy?
Mutation?
Template functions?
Unnecessary subscriptions?
Too many validators?
No debouncing?
Heavy DOM?
Change detection triggered by zone?
Angular performance problems are 80% architecture mistakes, not framework limits
After mastering this, you can:
✔ Optimize without trial-and-error
✔ Explain why a fix works
✔ Design forms that scale to 100+ controls
✔ Debug CD like a profiler, not a guesser
No comments:
Post a Comment