원클릭으로
change-detection
Deep dive into Angular change detection optimization, OnPush strategy, Signals, and Zone.js patterns
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Deep dive into Angular change detection optimization, OnPush strategy, Signals, and Zone.js patterns
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Proven architectural patterns for building scalable Angular applications in enterprise environments with large teams
Doguhan Uluca's Router-First Architecture - The 7 steps for designing scalable Angular applications by defining routes before components
Essential RxJS operators and patterns for Angular development - transformation, filtering, combination, and error handling operators
Angular Signals - Modern reactive state management patterns with computed signals, effects, and interoperability with RxJS
Comprehensive guide to Angular bundle optimization, code splitting, tree shaking, and lazy loading strategies
Secure authentication and authorization patterns including JWT, OAuth2, guards, and role-based access control
| name | change-detection |
| description | Deep dive into Angular change detection optimization, OnPush strategy, Signals, and Zone.js patterns |
Comprehensive guide to Angular change detection, OnPush strategy, Signals, and Zone.js optimization.
Angular uses Zone.js to detect async operations and trigger change detection:
// Any of these trigger change detection:
setTimeout(() => {}); // ✓ Triggers CD
setInterval(() => {}, 1000); // ✓ Triggers CD
fetch('/api/data'); // ✓ Triggers CD
button.addEventListener('click'); // ✓ Triggers CD
Promise.resolve(); // ✓ Triggers CD
App Component
/ \
/ \
Header Main
/ \
/ \
Sidebar Content
/ \
/ \
List Detail
Default Strategy: When CD runs, Angular checks every component in the tree.
@Component({
selector: 'app-user-list',
template: `
<div>{{ currentTime }}</div>
<button (click)="refresh()">Refresh</button>
`
})
export class UserListComponent {
currentTime = new Date().toLocaleTimeString();
refresh() {
this.currentTime = new Date().toLocaleTimeString();
}
}
Behavior:
@Component({
selector: 'app-user-list',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div>{{ currentTime }}</div>
<button (click)="refresh()">Refresh</button>
`
})
export class UserListComponent {
currentTime = new Date().toLocaleTimeString();
refresh() {
this.currentTime = new Date().toLocaleTimeString();
}
}
Behavior:
// Scenario: 100 components, 10 CD cycles/second
// Default Strategy:
// 100 components × 10 cycles × 60 seconds = 60,000 checks/minute
// OnPush Strategy:
// ~5 components × 10 cycles × 60 seconds = 3,000 checks/minute
// ⚡ 95% reduction!
@Component({
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProductListComponent {
@Input() products: Product[] = [];
// ❌ BAD: Mutates array (OnPush won't detect)
addProductBad(product: Product) {
this.products.push(product);
}
// ✅ GOOD: New array reference
addProductGood(product: Product) {
this.products = [...this.products, product];
}
// ✅ GOOD: Immutable update
updateProduct(id: string, updates: Partial<Product>) {
this.products = this.products.map(p =>
p.id === id ? { ...p, ...updates } : p
);
}
}
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- ✅ Async pipe triggers OnPush automatically -->
<div *ngFor="let user of users$ | async">
{{ user.name }}
</div>
`
})
export class UserListComponent {
users$ = this.userService.getUsers();
constructor(private userService: UserService) {}
}
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<button (click)="increment()">{{ count }}</button>
`
})
export class CounterComponent {
count = 0;
// ✅ Event handlers in template trigger OnPush
increment() {
this.count++;
}
}
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div>{{ count() }}</div>
<button (click)="increment()">+</button>
`
})
export class CounterComponent {
count = signal(0);
increment() {
this.count.update(c => c + 1);
}
}
// Create signal
const count = signal(0);
// Read signal
console.log(count()); // 0
// Update signal
count.set(5);
count.update(c => c + 1);
// Computed signal (derived state)
const doubled = computed(() => count() * 2);
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<h2>Cart ({{ itemCount() }} items)</h2>
<div>Total: {{ total() | currency }}</div>
<div *ngFor="let item of items()">
{{ item.name }} - {{ item.price | currency }}
<button (click)="removeItem(item.id)">Remove</button>
</div>
`
})
export class CartComponent {
items = signal<CartItem[]>([]);
itemCount = computed(() => this.items().length);
total = computed(() =>
this.items().reduce((sum, item) => sum + item.price, 0)
);
addItem(item: CartItem) {
this.items.update(items => [...items, item]);
}
removeItem(id: string) {
this.items.update(items => items.filter(i => i.id !== id));
}
}
export class UserComponent {
userId = signal<string | null>(null);
user = signal<User | null>(null);
constructor() {
// Effect runs when dependencies change
effect(() => {
const id = this.userId();
if (id) {
this.userService.getUser(id).subscribe(
user => this.user.set(user)
);
}
});
}
}
// Observable approach
export class ProductsComponent {
private searchTerm$ = new BehaviorSubject('');
products$ = this.searchTerm$.pipe(
debounceTime(300),
switchMap(term => this.productService.search(term))
);
search(term: string) {
this.searchTerm$.next(term);
}
}
// Signal approach
export class ProductsComponent {
searchTerm = signal('');
products = computed(() => {
// Note: computed is synchronous
// For async, combine with effect
return this.productService.search(this.searchTerm());
});
search(term: string) {
this.searchTerm.set(term);
}
}
// Hybrid approach (best for now)
export class ProductsComponent {
searchTerm = signal('');
products$ = toObservable(this.searchTerm).pipe(
debounceTime(300),
switchMap(term => this.productService.search(term))
);
}
export class ChartComponent {
constructor(private ngZone: NgZone) {}
startAnimation() {
// Run outside Angular's zone (no CD triggered)
this.ngZone.runOutsideAngular(() => {
const animate = () => {
// Heavy animation logic
this.updateChart();
requestAnimationFrame(animate);
};
requestAnimationFrame(animate);
});
}
// Manually trigger CD when needed
updateData(data: any) {
this.ngZone.run(() => {
this.chartData = data;
});
}
}
export class LiveDataComponent implements OnInit, OnDestroy {
data: any;
private interval: any;
constructor(private ngZone: NgZone) {}
ngOnInit() {
// Poll every second without triggering CD
this.ngZone.runOutsideAngular(() => {
this.interval = setInterval(() => {
this.fetchData().then(data => {
// Only trigger CD when data arrives
this.ngZone.run(() => {
this.data = data;
});
});
}, 1000);
});
}
ngOnDestroy() {
clearInterval(this.interval);
}
}
// main.ts
bootstrapApplication(AppComponent, {
providers: [
provideExperimentalZonelessChangeDetection()
]
});
// Component must use OnPush + Signals
@Component({
changeDetection: ChangeDetectionStrategy.OnPush
})
export class MyComponent {
count = signal(0); // Signals work without Zone.js
}
@Component({
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ManualComponent {
data: any;
constructor(private cdr: ChangeDetectorRef) {}
// Method 1: detectChanges() - Check this component
updateData(data: any) {
this.data = data;
this.cdr.detectChanges();
}
// Method 2: markForCheck() - Mark for next CD cycle
scheduleUpdate(data: any) {
this.data = data;
this.cdr.markForCheck();
}
// Method 3: detach() - Stop automatic CD
ngOnInit() {
this.cdr.detach();
// Manual control
setInterval(() => {
this.data = new Date();
this.cdr.detectChanges();
}, 1000);
}
// Method 4: reattach() - Resume automatic CD
enableAutoDetection() {
this.cdr.reattach();
}
}
// ✅ Use Case 1: Third-party lib updates
export class MapComponent {
constructor(private cdr: ChangeDetectorRef) {}
initMap() {
this.mapLibrary.on('update', (data) => {
this.mapData = data;
this.cdr.markForCheck(); // Tell Angular to check
});
}
}
// ✅ Use Case 2: WebSocket updates
export class LiveFeedComponent {
constructor(
private cdr: ChangeDetectorRef,
private ws: WebSocketService
) {}
ngOnInit() {
this.ws.messages$.subscribe(msg => {
this.messages.push(msg);
this.cdr.markForCheck();
});
}
}
// ✅ Use Case 3: Performance-critical updates
export class GameComponent {
constructor(private cdr: ChangeDetectorRef) {}
ngOnInit() {
this.cdr.detach(); // Stop auto CD
// Game loop
const loop = () => {
this.updateGame();
// Only trigger CD when rendering
if (this.shouldRender) {
this.cdr.detectChanges();
}
requestAnimationFrame(loop);
};
requestAnimationFrame(loop);
}
}
// ❌ BAD: Parent change won't trigger OnPush
@Component({
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChildComponent {
@Input() user: User;
}
// Parent mutates object
this.user.name = 'Updated'; // OnPush child won't detect
// ✅ GOOD: Create new reference
this.user = { ...this.user, name: 'Updated' };
// ❌ BAD: Nested mutation
@Input() config = { theme: { color: 'blue' } };
this.config.theme.color = 'red'; // Won't trigger OnPush
// ✅ GOOD: Deep immutable update
this.config = {
...this.config,
theme: { ...this.config.theme, color: 'red' }
};
// ❌ BAD: Mutating array
@Input() items: string[] = [];
this.items.push('new'); // Won't trigger
this.items.splice(0, 1); // Won't trigger
this.items[0] = 'updated'; // Won't trigger
// ✅ GOOD: New array
this.items = [...this.items, 'new'];
this.items = this.items.filter((_, i) => i !== 0);
this.items = this.items.map((item, i) => i === 0 ? 'updated' : item);
// ❌ BAD: Manual subscription in OnPush
@Component({
changeDetection: ChangeDetectionStrategy.OnPush
})
export class BadComponent {
users: User[] = [];
ngOnInit() {
this.userService.getUsers().subscribe(users => {
this.users = users; // Won't trigger OnPush!
});
}
}
// ✅ GOOD: Use async pipe
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<div *ngFor="let user of users$ | async">...</div>`
})
export class GoodComponent {
users$ = this.userService.getUsers();
}
// ✅ GOOD: Or use markForCheck
@Component({
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AlsoGoodComponent {
users: User[] = [];
constructor(private cdr: ChangeDetectorRef) {}
ngOnInit() {
this.userService.getUsers().subscribe(users => {
this.users = users;
this.cdr.markForCheck();
});
}
}
// 1. Open Chrome DevTools
// 2. Performance tab
// 3. Click Record
// 4. Interact with app
// 5. Stop recording
// Look for:
// - Long tasks (>50ms)
// - Excessive change detection
// - Layout thrashing
// 1. Install Angular DevTools extension
// 2. Open DevTools > Angular tab
// 3. Profiler section
// 4. Record profile
// 5. Analyze change detection cycles
// Shows:
// - Change detection timing
// - Component tree
// - Change detection strategy per component
export class ProfiledComponent {
loadData() {
performance.mark('data-load-start');
this.service.getData().subscribe(data => {
this.data = data;
performance.mark('data-load-end');
performance.measure(
'Data Load',
'data-load-start',
'data-load-end'
);
// View in Performance tab
const measure = performance.getEntriesByName('Data Load')[0];
console.log(`Data load took ${measure.duration}ms`);
});
}
}
// Directive to count CD cycles
@Directive({
selector: '[cdCounter]',
standalone: true
})
export class CdCounterDirective implements DoCheck {
private count = 0;
ngDoCheck() {
this.count++;
console.log(`CD cycle #${this.count}`);
}
}
// Usage
<app-my-component cdCounter></app-my-component>
✅ DO:
ChangeDetectionStrategy.OnPush by defaultasync pipe for observablestrackBy for listsmarkForCheck() for third-party integrations❌ DON'T:
@Input() propertiesmarkForCheck()✓ @Input() reference changes
✓ Event from template
✓ async pipe emission
✓ cdr.detectChanges()
✓ cdr.markForCheck()
✓ Signal updates (in templates)
✗ Object mutation
✗ Array.push/splice/pop
✗ Nested property changes
✗ Manual subscription
✗ setTimeout/setInterval (without events)
| Feature | Default | OnPush |
|---|---|---|
| Checks on every CD | ✓ | ✗ |
| Checks on @Input change | ✓ | ✓ (reference) |
| Checks on events | ✓ | ✓ |
| Checks on async pipe | ✓ | ✓ |
| Performance | Low | High |
| Complexity | Low | Medium |
Master change detection for blazing-fast Angular apps! 🚀