Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:25:00 +08:00
commit b91096af16
8 changed files with 2421 additions and 0 deletions

View File

@@ -0,0 +1,755 @@
# Angular Bundle Optimization
Complete guide to reducing Angular bundle sizes through code splitting, tree shaking, and lazy loading.
## Table of Contents
1. [Bundle Analysis](#bundle-analysis)
2. [Lazy Loading Strategies](#lazy-loading-strategies)
3. [Tree Shaking](#tree-shaking)
4. [Code Splitting](#code-splitting)
5. [Library Optimization](#library-optimization)
6. [Build Configuration](#build-configuration)
7. [Image Optimization](#image-optimization)
8. [Caching Strategies](#caching-strategies)
---
## Bundle Analysis
### Generate Bundle Stats
```bash
# Build with statistics
ng build --configuration production --stats-json
# Output: dist/<project>/stats.json
```
### Analyze with Tools
```bash
# Webpack Bundle Analyzer
npm install --save-dev webpack-bundle-analyzer
npx webpack-bundle-analyzer dist/<project>/stats.json
# Source Map Explorer
npm install --save-dev source-map-explorer
ng build --configuration production --source-map
npx source-map-explorer dist/**/*.js
# Bundle Buddy
npx bundle-buddy dist/<project>/stats.json
```
### Reading the Analysis
```
main.js (1.2 MB)
├── @angular/core (280 KB)
├── @angular/common (150 KB)
├── lodash (287 KB) ⚠️ Can optimize
├── moment (67 KB) ⚠️ Can replace
├── rxjs (98 KB)
└── application code (318 KB)
```
---
## Lazy Loading Strategies
### Route-Based Lazy Loading
```typescript
// app.routes.ts
export const routes: Routes = [
{
path: 'dashboard',
loadComponent: () => import('./dashboard/dashboard.component')
.then(m => m.DashboardComponent)
},
{
path: 'users',
loadChildren: () => import('./users/users.routes')
.then(m => m.USERS_ROUTES)
},
{
path: 'admin',
loadChildren: () => import('./admin/admin.routes')
.then(m => m.ADMIN_ROUTES),
canActivate: [AuthGuard] // Only loads if authorized
}
];
```
**Impact**: Main bundle reduced by 40-60%
### Component Lazy Loading
```typescript
// Before: Imported at module level
import { HeavyChartComponent } from './chart/heavy-chart.component';
@Component({
template: `
<app-heavy-chart *ngIf="showChart" [data]="chartData" />
`
})
export class DashboardComponent {
showChart = false;
}
// After: Dynamic import
@Component({
template: `
<ng-container *ngIf="chartComponent">
<ng-container *ngComponentOutlet="chartComponent; inputs: chartInputs" />
</ng-container>
`
})
export class DashboardComponent {
chartComponent: any;
chartInputs = { data: [] };
async loadChart() {
const { HeavyChartComponent } = await import('./chart/heavy-chart.component');
this.chartComponent = HeavyChartComponent;
}
}
```
### Preloading Strategies
```typescript
// app.config.ts
import { PreloadAllModules, NoPreloading } from '@angular/router';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(
routes,
// Option 1: No preloading (default)
withPreloading(NoPreloading),
// Option 2: Preload all
withPreloading(PreloadAllModules),
// Option 3: Custom preloading
withPreloading(CustomPreloadingStrategy)
)
]
};
// Custom preloading strategy
@Injectable({ providedIn: 'root' })
export class CustomPreloadingStrategy implements PreloadingStrategy {
preload(route: Route, load: () => Observable<any>): Observable<any> {
// Preload only routes with data.preload = true
return route.data?.['preload'] ? load() : of(null);
}
}
// Route configuration
{
path: 'important',
loadChildren: () => import('./important/routes'),
data: { preload: true } // Will preload
}
```
### Lazy Load on Interaction
```typescript
@Component({
template: `
<button (click)="openDialog()">Open Settings</button>
`
})
export class AppComponent {
async openDialog() {
// Load dialog only when button clicked
const { SettingsDialogComponent } = await import(
'./settings/settings-dialog.component'
);
const dialogRef = this.dialog.open(SettingsDialogComponent);
}
}
```
---
## Tree Shaking
### How Tree Shaking Works
Tree shaking removes unused code during the build process.
```typescript
// library.ts
export function usedFunction() { /* ... */ }
export function unusedFunction() { /* ... */ }
// app.ts
import { usedFunction } from './library';
usedFunction();
// unusedFunction is removed from bundle ✂️
```
### Optimize Imports
```typescript
// ❌ BAD: Imports entire library
import * as _ from 'lodash';
import * as moment from 'moment';
_.debounce(fn, 300);
moment().format('YYYY-MM-DD');
// ✅ GOOD: Import only what you need
import { debounce } from 'lodash-es';
import { format } from 'date-fns';
debounce(fn, 300);
format(new Date(), 'yyyy-MM-dd');
```
### Material Components
```typescript
// ❌ BAD: Import entire Material module
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatTableModule } from '@angular/material/table';
import { MatPaginatorModule } from '@angular/material/paginator';
// ... 20+ more imports in a shared module
// ✅ GOOD: Import per component/feature
@Component({
standalone: true,
imports: [
MatButtonModule, // Only button needed here
CommonModule
]
})
export class SimpleComponent { }
```
### providedIn: 'root' for Services
```typescript
// ❌ BAD: Service in module providers
@NgModule({
providers: [DataService] // Always in bundle
})
// ✅ GOOD: Tree-shakeable service
@Injectable({
providedIn: 'root' // Only in bundle if used
})
export class DataService { }
```
### Side-Effect-Free Code
```typescript
// ❌ BAD: Side effects prevent tree shaking
export class Logger {
constructor() {
console.log('Logger initialized'); // Side effect!
}
}
// Even if unused, stays in bundle
// ✅ GOOD: No side effects
export class Logger {
log(message: string) {
console.log(message);
}
}
```
---
## Code Splitting
### Manual Chunks
```typescript
// angular.json
{
"projects": {
"app": {
"architect": {
"build": {
"configurations": {
"production": {
"optimization": {
"scripts": true,
"styles": {
"minify": true
}
},
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
}
],
"namedChunks": false,
"outputHashing": "all"
}
}
}
}
}
}
}
```
### Vendor Chunking
Angular automatically creates vendor chunks:
```
dist/
├── main.js (Your code)
├── vendor.js (node_modules)
├── polyfills.js (Browser polyfills)
└── runtime.js (Webpack runtime)
```
### Custom Webpack Config
```typescript
// custom-webpack.config.ts
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
priority: 10
},
common: {
minChunks: 2,
priority: 5,
reuseExistingChunk: true
},
// Separate heavy libraries
charts: {
test: /[\\/]node_modules[\\/](chart\.js|d3)[\\/]/,
name: 'charts',
priority: 15
}
}
}
}
};
// angular.json
{
"architect": {
"build": {
"builder": "@angular-builders/custom-webpack:browser",
"options": {
"customWebpackConfig": {
"path": "./custom-webpack.config.ts"
}
}
}
}
}
```
---
## Library Optimization
### Replace Heavy Libraries
```typescript
// ❌ Moment.js (67 KB gzipped)
import * as moment from 'moment';
const date = moment().format('YYYY-MM-DD');
// ✅ date-fns (6 KB gzipped)
import { format } from 'date-fns';
const date = format(new Date(), 'yyyy-MM-dd');
// ✅ Native Intl API (0 KB - built-in)
const date = new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
}).format(new Date());
// ❌ Lodash (24 KB gzipped)
import _ from 'lodash';
// ✅ Lodash-es (tree-shakeable)
import { debounce, throttle } from 'lodash-es';
// ✅ Native alternatives
const unique = [...new Set(array)];
const grouped = Object.groupBy(array, item => item.category);
```
### Polyfill Only What's Needed
```typescript
// polyfills.ts
// ❌ Import everything
import 'core-js';
// ✅ Import selectively
import 'core-js/es/array/flat';
import 'core-js/es/array/flat-map';
import 'core-js/es/string/replace-all';
// Or use browserslist to auto-determine
```
### Check Before Adding
```bash
# Check package size before installing
npx bundlephobia <package-name>
# Example output:
# lodash: 72.4 KB (24.2 KB gzipped)
# lodash-es: 72.4 KB (tree-shakeable!)
# date-fns: 78 KB (6 KB gzipped per function)
```
---
## Build Configuration
### Production Optimizations
```json
// angular.json
{
"configurations": {
"production": {
"optimization": {
"scripts": true,
"styles": {
"minify": true,
"inlineCritical": true
},
"fonts": true
},
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"buildOptimizer": true,
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
]
}
}
}
```
### Bundle Budgets
```json
{
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "6kb",
"maximumError": "10kb"
},
{
"type": "bundle",
"name": "vendor",
"maximumWarning": "2mb",
"maximumError": "3mb"
}
]
}
```
### Differential Loading
Angular automatically generates ES5 and ES2015+ bundles:
```html
<!-- Modern browsers (95% of users) -->
<script src="main-es2015.js" type="module"></script>
<!-- Legacy browsers (5% of users) -->
<script src="main-es5.js" nomodule></script>
```
**Impact**: Modern browsers download 30-40% less code
### AOT Compilation
```json
// Always use AOT in production
{
"aot": true, // Ahead-of-Time compilation
"buildOptimizer": true // Additional optimizations
}
```
**Benefits**:
- Faster rendering (templates pre-compiled)
- Smaller bundles (compiler not included)
- Early error detection
---
## Image Optimization
### Modern Formats
```html
<!-- Use WebP with fallback -->
<picture>
<source srcset="image.webp" type="image/webp">
<source srcset="image.jpg" type="image/jpeg">
<img src="image.jpg" alt="Description">
</picture>
```
### Responsive Images
```html
<img
srcset="
image-320w.jpg 320w,
image-640w.jpg 640w,
image-1280w.jpg 1280w
"
sizes="(max-width: 320px) 280px,
(max-width: 640px) 600px,
1200px"
src="image-640w.jpg"
alt="Description"
>
```
### Lazy Loading
```html
<!-- Native lazy loading -->
<img src="image.jpg" loading="lazy" alt="Description">
<!-- With Angular -->
<img
[src]="imageUrl"
loading="lazy"
[width]="300"
[height]="200"
alt="Description"
>
```
### Image CDN
```typescript
// Use ImageKit, Cloudinary, or similar
const optimizedUrl = `https://ik.imagekit.io/demo/tr:w-400,h-300,f-webp/${imagePath}`;
// Transformations:
// tr:w-400,h-300 = resize to 400x300
// f-webp = convert to WebP
// q-80 = quality 80%
```
---
## Caching Strategies
### Service Worker
```bash
# Add Angular PWA
ng add @angular/pwa
```
```json
// ngsw-config.json
{
"index": "/index.html",
"assetGroups": [
{
"name": "app",
"installMode": "prefetch",
"resources": {
"files": [
"/favicon.ico",
"/index.html",
"/*.css",
"/*.js"
]
}
},
{
"name": "assets",
"installMode": "lazy",
"updateMode": "prefetch",
"resources": {
"files": [
"/assets/**",
"/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2)"
]
}
}
],
"dataGroups": [
{
"name": "api",
"urls": ["/api/**"],
"cacheConfig": {
"maxSize": 100,
"maxAge": "1h",
"timeout": "10s",
"strategy": "freshness"
}
}
]
}
```
### HTTP Caching Headers
```typescript
// HTTP interceptor
export const cacheInterceptor: HttpInterceptorFn = (req, next) => {
// Add cache headers for static assets
if (req.url.includes('/api/static/')) {
req = req.clone({
setHeaders: {
'Cache-Control': 'public, max-age=31536000, immutable'
}
});
}
return next(req);
};
```
### Browser Caching
```typescript
// Output hashing (angular.json)
{
"outputHashing": "all" // Generates unique filenames
}
// Result:
// main.abc123.js
// vendor.def456.js
// Enables long-term caching!
```
---
## Optimization Checklist
### Quick Wins
- [ ] Enable production mode
- [ ] Lazy load all feature routes
- [ ] Use lodash-es instead of lodash
- [ ] Replace moment.js with date-fns
- [ ] Enable OnPush change detection
- [ ] Add service worker for caching
- [ ] Optimize images (WebP, lazy loading)
### Medium Impact
- [ ] Analyze bundle with webpack-bundle-analyzer
- [ ] Remove unused dependencies
- [ ] Import Material components selectively
- [ ] Set up bundle budgets
- [ ] Implement preloading strategy
- [ ] Use trackBy for lists
- [ ] Add virtual scrolling for large lists
### Advanced
- [ ] Custom Webpack configuration
- [ ] Image CDN integration
- [ ] HTTP/2 push
- [ ] Critical CSS inlining
- [ ] SSR/Prerendering
- [ ] Compression (Brotli/Gzip)
---
## Measuring Success
### Before Optimization
```
Bundle Sizes:
- main.js: 1.2 MB (380 KB gzipped)
- vendor.js: 850 KB (280 KB gzipped)
- Total: 2.05 MB (660 KB gzipped)
Lighthouse Score: 67
- FCP: 2.1s
- LCP: 3.5s
- TTI: 4.2s
```
### After Optimization
```
Bundle Sizes:
- main.js: 420 KB (135 KB gzipped)
- vendor.js: 580 KB (180 KB gzipped)
- dashboard.js: 120 KB (lazy)
- users.js: 95 KB (lazy)
- Total Initial: 1 MB (315 KB gzipped)
Lighthouse Score: 98
- FCP: 0.8s
- LCP: 1.2s
- TTI: 1.5s
Improvement:
- Bundle size: ⬇️ 51% reduction
- Load time: ⬇️ 64% faster
- Performance score: ⬆️ 46% better
```
---
## Tools Reference
```bash
# Analysis
npx webpack-bundle-analyzer dist/stats.json
npx source-map-explorer dist/**/*.js
npx bundlephobia <package>
# Building
ng build --configuration production --stats-json
ng build --source-map
# Testing
npm run lighthouse
npx @unlighthouse/cli --site http://localhost:4200
```
---
*Optimize bundles for lightning-fast loads! ⚡📦*

View File

@@ -0,0 +1,744 @@
# Angular Change Detection Optimization
Comprehensive guide to Angular change detection, OnPush strategy, Signals, and Zone.js optimization.
## Table of Contents
1. [How Change Detection Works](#how-change-detection-works)
2. [Default vs OnPush](#default-vs-onpush)
3. [OnPush Strategy Patterns](#onpush-strategy-patterns)
4. [Angular Signals](#angular-signals)
5. [Zone.js Optimization](#zonejs-optimization)
6. [Manual Change Detection](#manual-change-detection)
7. [Common Pitfalls](#common-pitfalls)
8. [Performance Monitoring](#performance-monitoring)
---
## How Change Detection Works
### The Basics
Angular uses **Zone.js** to detect async operations and trigger change detection:
```typescript
// 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
```
### The Component Tree
```
App Component
/ \
/ \
Header Main
/ \
/ \
Sidebar Content
/ \
/ \
List Detail
```
**Default Strategy**: When CD runs, Angular checks **every component** in the tree.
### Change Detection Cycle
1. **Event occurs** (click, HTTP response, timer)
2. **Zone.js intercepts** the async operation
3. **Angular runs change detection** from root
4. **Each component** checks if bindings changed
5. **DOM updates** if changes detected
---
## Default vs OnPush
### Default Strategy
```typescript
@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**:
- Checks on **every** CD cycle
- Even if data didn't change
- Performance cost: **High** (scales with components)
### OnPush Strategy
```typescript
@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**:
- Only checks when:
1. **@Input() reference changes**
2. **Event handler** in component template
3. **Observable emits** with async pipe
4. **Manual detection** triggered
- Performance cost: **Low** (90% reduction)
### Performance Comparison
```typescript
// 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!
```
---
## OnPush Strategy Patterns
### Pattern 1: Immutable Data
```typescript
@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
);
}
}
```
### Pattern 2: Async Pipe
```typescript
@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) {}
}
```
### Pattern 3: Event Handlers
```typescript
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<button (click)="increment()">{{ count }}</button>
`
})
export class CounterComponent {
count = 0;
// ✅ Event handlers in template trigger OnPush
increment() {
this.count++;
}
}
```
### Pattern 4: Signals (Angular 16+)
```typescript
@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);
}
}
```
---
## Angular Signals
### Signal Basics
```typescript
// 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);
```
### Signals in Components
```typescript
@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));
}
}
```
### Signal Effects
```typescript
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)
);
}
});
}
}
```
### Signals vs Observables
```typescript
// 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))
);
}
```
---
## Zone.js Optimization
### Run Outside Zone
```typescript
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;
});
}
}
```
### Polling Outside Zone
```typescript
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);
}
}
```
### Zone-less Angular (Experimental)
```typescript
// 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
}
```
---
## Manual Change Detection
### ChangeDetectorRef
```typescript
@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();
}
}
```
### When to Use Manual Detection
```typescript
// ✅ 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);
}
}
```
---
## Common Pitfalls
### Pitfall 1: Mutating @Input
```typescript
// ❌ 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' };
```
### Pitfall 2: Nested Objects
```typescript
// ❌ 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' }
};
```
### Pitfall 3: Array Modifications
```typescript
// ❌ 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);
```
### Pitfall 4: Async without async pipe
```typescript
// ❌ 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();
});
}
}
```
---
## Performance Monitoring
### Chrome DevTools Profiler
```typescript
// 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
```
### Angular DevTools
```typescript
// 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
```
### Performance Marks
```typescript
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`);
});
}
}
```
### Change Detection Counter
```typescript
// 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>
```
---
## Best Practices Summary
**DO**:
- Use `ChangeDetectionStrategy.OnPush` by default
- Use `async` pipe for observables
- Use Angular Signals for reactive state
- Create new references for objects/arrays
- Use `trackBy` for lists
- Run heavy operations outside Zone
- Use `markForCheck()` for third-party integrations
**DON'T**:
- Mutate `@Input()` properties
- Manually subscribe in OnPush without `markForCheck()`
- Use Default strategy unless necessary
- Perform heavy computations in getters
- Forget to unsubscribe from observables
- Mix imperative and reactive patterns
---
## Quick Reference
### Change Detection Triggers (OnPush)
```typescript
@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)
```
### Strategy Comparison
| 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! 🚀*