From b91096af16c25d7cc165da6a7fdc6b7880ceb919 Mon Sep 17 00:00:00 2001 From: Zhongwei Li Date: Sat, 29 Nov 2025 18:25:00 +0800 Subject: [PATCH] Initial commit --- .claude-plugin/plugin.json | 20 + README.md | 3 + agents/angular-performance-optimizer.md | 113 ++++ commands/analyze-bundle.md | 359 +++++++++++ commands/optimize-component.md | 366 ++++++++++++ plugin.lock.json | 61 ++ skills/bundle-optimization/SKILL.md | 755 ++++++++++++++++++++++++ skills/change-detection/SKILL.md | 744 +++++++++++++++++++++++ 8 files changed, 2421 insertions(+) create mode 100644 .claude-plugin/plugin.json create mode 100644 README.md create mode 100644 agents/angular-performance-optimizer.md create mode 100644 commands/analyze-bundle.md create mode 100644 commands/optimize-component.md create mode 100644 plugin.lock.json create mode 100644 skills/bundle-optimization/SKILL.md create mode 100644 skills/change-detection/SKILL.md diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..9e6e9ea --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,20 @@ +{ + "name": "angular-performance", + "description": "Performance optimization with OnPush, bundle analysis, and change detection strategies", + "version": "1.0.0", + "author": { + "name": "Ihsan - Full-Stack Developer & AI Strategist", + "url": "https://github.com/EhssanAtassi" + }, + "skills": [ + "./skills/change-detection/SKILL.md", + "./skills/bundle-optimization/SKILL.md" + ], + "agents": [ + "./agents/angular-performance-optimizer.md" + ], + "commands": [ + "./commands/optimize-component.md", + "./commands/analyze-bundle.md" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..fcfb72a --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# angular-performance + +Performance optimization with OnPush, bundle analysis, and change detection strategies diff --git a/agents/angular-performance-optimizer.md b/agents/angular-performance-optimizer.md new file mode 100644 index 0000000..dd23324 --- /dev/null +++ b/agents/angular-performance-optimizer.md @@ -0,0 +1,113 @@ +# Angular Performance Optimizer + +Expert in Angular performance optimization, bundle size reduction, and runtime efficiency. + +## Expertise + +- **Change Detection**: OnPush strategy, Signals, Zone.js optimization +- **Bundle Optimization**: Code splitting, tree shaking, lazy loading +- **Runtime Performance**: TrackBy, pure pipes, memo patterns +- **Memory Management**: Subscription cleanup, memory leak prevention +- **Build Optimization**: AOT, production configs, prerendering +- **Network Performance**: HTTP caching, compression, CDN strategies +- **Rendering Performance**: Virtual scrolling, image lazy loading + +## Core Responsibilities + +1. **Analyze performance bottlenecks** in Angular applications +2. **Optimize change detection** with OnPush and Signals +3. **Reduce bundle sizes** through code splitting and tree shaking +4. **Improve runtime performance** with trackBy, memoization, virtual scrolling +5. **Eliminate memory leaks** from subscriptions and DOM references +6. **Configure production builds** for maximum efficiency +7. **Implement caching strategies** for HTTP and data + +## Available Commands + +- `/angular-performance:optimize-component` - Optimize component performance +- `/angular-performance:analyze-bundle` - Analyze and reduce bundle size + +## Available Skills + +- `change-detection` - Deep dive into change detection optimization +- `bundle-optimization` - Bundle size reduction strategies + +## Optimization Philosophy + +- **Measure first** - Use Chrome DevTools and Lighthouse +- **Progressive enhancement** - Don't over-optimize prematurely +- **User-centric metrics** - Focus on LCP, FID, CLS +- **Balance trade-offs** - Performance vs maintainability + +## Code Quality Standards + +1. **OnPush by default** for all presentational components +2. **TrackBy functions** for all *ngFor loops +3. **Unsubscribe properly** using takeUntilDestroyed() +4. **Lazy load routes** for all feature modules +5. **Pure pipes** for data transformations +6. **Virtual scrolling** for large lists +7. **Image optimization** with lazy loading and WebP + +## Communication Style + +- Provide **concrete metrics** and benchmarks +- Show **before/after** performance comparisons +- Include **Chrome DevTools** profiling guidance +- Reference **Web Vitals** when relevant +- Give **actionable recommendations** prioritized by impact + +## Example Interaction + +**User**: "My user list is slow with 10,000 items" + +**You**: +```typescript +// Before: Default change detection, no trackBy +@Component({ + selector: 'app-user-list', + template: ` +
+ {{ user.name }} +
+ ` +}) +export class UserListComponent { + @Input() users: User[] = []; +} + +// After: OnPush + trackBy + virtual scrolling +@Component({ + selector: 'app-user-list', + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + +
+ {{ user.name }} +
+
+ `, + imports: [CdkVirtualScrollViewport, CdkVirtualForOf] +}) +export class UserListComponent { + @Input() users: User[] = []; + + trackById(index: number, user: User): string { + return user.id; + } +} +``` + +**Performance Impact:** +- 🚀 **Initial render**: 850ms → 45ms (94% faster) +- 🚀 **Scroll performance**: 60 FPS maintained +- 🚀 **Memory usage**: 120MB → 25MB (79% reduction) + +**Why it works:** +1. OnPush reduces change detection cycles +2. trackBy prevents unnecessary DOM updates +3. Virtual scrolling renders only visible items + +--- + +*Measure, optimize, validate - repeat! 📊* diff --git a/commands/analyze-bundle.md b/commands/analyze-bundle.md new file mode 100644 index 0000000..ca26c48 --- /dev/null +++ b/commands/analyze-bundle.md @@ -0,0 +1,359 @@ +# Analyze Bundle Command + +Analyze, visualize, and optimize Angular bundle sizes. + +## Usage + +```bash +/angular-performance:analyze-bundle +``` + +## What It Does + +1. Generate bundle statistics +2. Visualize bundle composition +3. Identify optimization opportunities +4. Provide actionable recommendations + +## Step 1: Generate Bundle Stats + +```bash +# Build with stats +ng build --configuration production --stats-json + +# Output: dist//stats.json +``` + +## Step 2: Analyze with Tools + +### Webpack Bundle Analyzer + +```bash +npm install --save-dev webpack-bundle-analyzer + +# Analyze +npx webpack-bundle-analyzer dist//stats.json +``` + +### Source Map Explorer + +```bash +npm install --save-dev source-map-explorer + +# Build with source maps +ng build --configuration production --source-map + +# Analyze +npx source-map-explorer dist/**/*.js +``` + +## Optimization Strategies + +### 1. Lazy Loading + +```typescript +// angular.json - before +const routes: Routes = [ + { path: 'dashboard', component: DashboardComponent }, + { path: 'users', component: UsersComponent }, + { path: 'reports', component: ReportsComponent } +]; + +// After - lazy load feature modules +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: 'reports', + loadChildren: () => import('./reports/reports.routes') + .then(m => m.REPORTS_ROUTES) + } +]; +``` + +**Impact**: Reduces initial bundle by 40-60% + +### 2. Tree Shaking + +```typescript +// Before: Imports entire library +import * as _ from 'lodash'; + +// After: Import only what you need +import { debounce, throttle } from 'lodash-es'; + +// Or use native alternatives +const debounce = (fn, delay) => { + let timeoutId; + return (...args) => { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => fn(...args), delay); + }; +}; +``` + +### 3. Remove Unused Dependencies + +```bash +# Find unused dependencies +npx depcheck + +# Analyze package size before adding +npx bundlephobia +``` + +### 4. Angular Material - Import Selectively + +```typescript +// Before: Import entire Material +import { MatButtonModule } from '@angular/material/button'; +import { MatCardModule } from '@angular/material/card'; +import { MatTableModule } from '@angular/material/table'; +// ... 20+ imports + +// After: Only import what you use per component +@Component({ + standalone: true, + imports: [MatButtonModule], // Only button for this component + template: '' +}) +``` + +### 5. Replace Heavy Libraries + +```typescript +// Before: Moment.js (67KB gzipped) +import * as moment from 'moment'; +const date = moment().format('YYYY-MM-DD'); + +// After: date-fns (6KB gzipped) +import { format } from 'date-fns'; +const date = format(new Date(), 'yyyy-MM-dd'); + +// Or native Intl API (0KB - built-in) +const date = new Intl.DateTimeFormat('en-US').format(new Date()); +``` + +### 6. Code Splitting with Dynamic Imports + +```typescript +// Before: Import at top level +import { ChartComponent } from './chart/chart.component'; + +export class DashboardComponent { + showChart = false; + chart = ChartComponent; // Always in bundle +} + +// After: Dynamic import +export class DashboardComponent { + showChart = false; + chartComponent: any; + + async loadChart() { + const { ChartComponent } = await import('./chart/chart.component'); + this.chartComponent = ChartComponent; + this.showChart = true; + } +} +``` + +### 7. Optimize Images + +```bash +# Install image optimization tools +npm install --save-dev imagemin imagemin-webp imagemin-pngquant + +# Use WebP format with fallbacks + + + Description + +``` + +### 8. Enable Build 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, + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kb", + "maximumError": "1mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb", + "maximumError": "10kb" + } + ] + } + } +} +``` + +### 9. Differential Loading + +Angular automatically generates ES5 and ES2015+ bundles: + +```html + + + + + +``` + +### 10. Service Worker & Caching + +```bash +# Add service worker +ng add @angular/pwa + +# Configures ngsw-config.json for caching +``` + +## Bundle Budget Enforcement + +```json +// angular.json - Set budgets +"budgets": [ + { + "type": "initial", + "maximumWarning": "500kb", + "maximumError": "1mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb", + "maximumError": "10kb" + } +] +``` + +## Analysis Report Example + +``` +📦 Bundle Analysis Report + +📊 Current Bundle Size: +┌─────────────────┬──────────┬──────────┐ +│ Chunk │ Size │ Gzipped │ +├─────────────────┼──────────┼──────────┤ +│ main.js │ 1.2 MB │ 380 KB │ +│ polyfills.js │ 145 KB │ 45 KB │ +│ runtime.js │ 12 KB │ 4 KB │ +│ styles.css │ 85 KB │ 15 KB │ +├─────────────────┼──────────┼──────────┤ +│ Total │ 1.44 MB │ 444 KB │ +└─────────────────┴──────────┴──────────┘ + +⚠️ Issues Found: + +1. 🔴 Lodash (287 KB) - Replace with lodash-es or native +2. 🟡 Moment.js (67 KB) - Replace with date-fns (6 KB) +3. 🟡 Feature modules not lazy loaded (420 KB in main) +4. 🟡 Angular Material fully imported (180 KB unused) + +✅ Recommended Actions: + +1. Lazy load feature modules → Save ~400 KB + - Dashboard, Users, Reports modules + +2. Replace heavy libraries: + - lodash → lodash-es → Save 250 KB + - moment.js → date-fns → Save 60 KB + +3. Tree-shake Material imports → Save 150 KB + - Import only used components + +4. Enable build optimizations → Save 100 KB + - Already in angular.json + +💡 Potential Savings: ~860 KB (59% reduction) + +📈 After Optimization: +┌─────────────────┬──────────┬──────────┐ +│ main.js │ 580 KB │ 180 KB │ +│ dashboard.js │ 120 KB │ 35 KB │ (lazy) +│ users.js │ 95 KB │ 28 KB │ (lazy) +│ reports.js │ 205 KB │ 62 KB │ (lazy) +├─────────────────┼──────────┼──────────┤ +│ Total Initial │ 580 KB │ 180 KB │ ⬇️ 60% +│ Total All │ 1.0 MB │ 305 KB │ ⬇️ 31% +└─────────────────┴──────────┴──────────┘ +``` + +## Continuous Monitoring + +### GitHub Action for Bundle Size + +```yaml +name: Bundle Size Check + +on: [pull_request] + +jobs: + check-size: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + - run: npm ci + - run: npm run build -- --stats-json + + - name: Analyze bundle + uses: github/webpack-bundle-analyzer@v1 + with: + bundle-stats: 'dist/stats.json' + + - name: Check size limits + run: | + SIZE=$(stat -f%z "dist/main.*.js") + if [ $SIZE -gt 500000 ]; then + echo "Bundle too large: $SIZE bytes" + exit 1 + fi +``` + +## Quick Wins Checklist + +- [ ] Enable production mode optimizations +- [ ] Lazy load all feature modules +- [ ] Replace moment.js with date-fns +- [ ] Use lodash-es instead of lodash +- [ ] Remove unused dependencies +- [ ] Optimize images (WebP, lazy loading) +- [ ] Import Material components selectively +- [ ] Enable differential loading +- [ ] Set up bundle budgets +- [ ] Add service worker for caching + +--- + +*Smaller bundles = Faster apps! 📦⚡* diff --git a/commands/optimize-component.md b/commands/optimize-component.md new file mode 100644 index 0000000..823e4c1 --- /dev/null +++ b/commands/optimize-component.md @@ -0,0 +1,366 @@ +# Optimize Component Command + +Analyze and optimize Angular component performance using OnPush, trackBy, memoization, and other strategies. + +## Usage + +```bash +/angular-performance:optimize-component +``` + +## Natural Language + +- "Optimize UserListComponent performance" +- "Make my dashboard component faster" +- "Reduce change detection in ProductCardComponent" + +## Optimization Strategies + +### 1. OnPush Change Detection + +```typescript +// Before +@Component({ + selector: 'app-product-list', + templateUrl: './product-list.component.html' +}) +export class ProductListComponent { + @Input() products: Product[] = []; +} + +// After +@Component({ + selector: 'app-product-list', + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: './product-list.component.html' +}) +export class ProductListComponent { + @Input() products: Product[] = []; +} +``` + +**Impact**: Reduces change detection checks by 80-90% + +### 2. TrackBy Functions + +```typescript +// Before +
+ {{ product.name }} +
+ +// After +@for (product of products; track product.id) { +
{{ product.name }}
+} + +// Or with trackBy function +trackById(index: number, item: Product): string { + return item.id; +} + +
+ {{ product.name }} +
+``` + +**Impact**: Prevents unnecessary DOM recreations, 60% faster re-renders + +### 3. Memoization with Signals + +```typescript +// Before: Computed on every change detection +get filteredProducts(): Product[] { + return this.products().filter(p => p.price > 100); +} + +// After: Memoized with computed signal +filteredProducts = computed(() => + this.products().filter(p => p.price > 100) +); +``` + +**Impact**: Computation runs only when dependencies change + +### 4. Pure Pipes + +```typescript +// Before: Impure pipe (runs on every CD) +@Pipe({ name: 'filter' }) +export class FilterPipe { + transform(items: any[], searchTerm: string): any[] { + return items.filter(item => + item.name.includes(searchTerm) + ); + } +} + +// After: Pure pipe with immutable data +@Pipe({ name: 'filter', pure: true }) +export class FilterPipe { + transform(items: any[], searchTerm: string): any[] { + return items.filter(item => + item.name.includes(searchTerm) + ); + } +} + +// Component: Use immutable operations +addProduct(product: Product) { + this.products = [...this.products, product]; // ✅ New reference + // NOT: this.products.push(product); // ❌ Mutates array +} +``` + +### 5. Virtual Scrolling + +```typescript +// Before: Renders all items +
+ +
+ +// After: Renders only visible items + + + +``` + +**Impact**: 95% faster rendering for large lists (>1000 items) + +### 6. Lazy Loading Images + +```typescript +// Before + + +// After + +``` + +### 7. Subscription Management + +```typescript +// Before: Manual unsubscribe +export class UserComponent implements OnInit, OnDestroy { + private subscription = new Subscription(); + + ngOnInit() { + this.subscription.add( + this.userService.getUser().subscribe(...) + ); + } + + ngOnDestroy() { + this.subscription.unsubscribe(); + } +} + +// After: takeUntilDestroyed() +export class UserComponent implements OnInit { + private destroyRef = inject(DestroyRef); + + ngOnInit() { + this.userService.getUser() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(...); + } +} + +// Or use async pipe (best) +user$ = this.userService.getUser(); +// Template: {{ user$ | async }} +``` + +### 8. Detach Change Detection + +```typescript +// For components that rarely update +export class StaticContentComponent implements OnInit { + constructor(private cdr: ChangeDetectorRef) {} + + ngOnInit() { + this.cdr.detach(); // Stop automatic change detection + } + + updateContent() { + // Manually trigger when needed + this.cdr.detectChanges(); + } +} +``` + +## Optimization Checklist + +```typescript +// Component optimization checklist +@Component({ + selector: 'app-optimized', + templateUrl: './optimized.component.html', + styleUrls: ['./optimized.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, // ✅ 1. OnPush + standalone: true, + imports: [CommonModule, CdkVirtualScrollViewport] +}) +export class OptimizedComponent { + // ✅ 2. Use signals for reactive state + private userService = inject(UserService); + users = signal([]); + + // ✅ 3. Computed for derived state + activeUsers = computed(() => + this.users().filter(u => u.active) + ); + + // ✅ 4. TrackBy function + trackById(index: number, user: User): string { + return user.id; + } + + ngOnInit() { + // ✅ 5. takeUntilDestroyed for subscriptions + this.userService.getUsers() + .pipe(takeUntilDestroyed()) + .subscribe(users => this.users.set(users)); + } +} +``` + +```html + + + +
+ + + + + {{ user.name }} +
+
+``` + +## Performance Metrics + +Measure with Chrome DevTools: + +```typescript +// Add performance marks +performance.mark('component-init-start'); +// ... component logic +performance.mark('component-init-end'); +performance.measure('Component Init', 'component-init-start', 'component-init-end'); +``` + +## Output Example + +``` +🎯 Component Optimization Report: UserListComponent + +📊 Before: +- Change Detection: Default (runs on every event) +- List Rendering: 2,500 items fully rendered +- Memory Usage: 145 MB +- Initial Render: 850ms +- FPS during scroll: 25-30 + +✅ After Optimizations: +1. ✓ OnPush change detection +2. ✓ trackBy function for list +3. ✓ Virtual scrolling (renders ~20 items) +4. ✓ Image lazy loading +5. ✓ Subscription cleanup with takeUntilDestroyed + +📈 Results: +- Change Detection: 92% reduction in checks +- List Rendering: 2,500 → 20 items (99% less DOM) +- Memory Usage: 145 MB → 28 MB (81% reduction) +- Initial Render: 850ms → 42ms (95% faster) +- FPS during scroll: 58-60 (smooth) + +🎁 Bonus: +- Lighthouse Performance Score: 67 → 98 +- First Contentful Paint: 2.1s → 0.8s +- Time to Interactive: 3.5s → 1.2s +``` + +## Common Patterns + +### Pattern 1: List with Filters + +```typescript +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + + + +
+ {{ item.name }} +
+
+ ` +}) +export class FilterableListComponent { + items = signal([]); + searchTerm = signal(''); + + filteredItems = computed(() => { + const term = this.searchTerm().toLowerCase(); + return this.items().filter(item => + item.name.toLowerCase().includes(term) + ); + }); + + trackById = (index: number, item: Item) => item.id; +} +``` + +### Pattern 2: Nested Components + +```typescript +// Parent: Smart component with OnPush +@Component({ + selector: 'app-user-dashboard', + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + + + ` +}) +export class UserDashboardComponent { + user = signal(null); + stats = signal(null); +} + +// Child: Dumb component with OnPush +@Component({ + selector: 'app-user-profile', + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +

{{ user.name }}

+ + ` +}) +export class UserProfileComponent { + @Input({ required: true }) user!: User; +} +``` + +--- + +*Optimize smart, measure always! 🚀* diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..0bb7cf9 --- /dev/null +++ b/plugin.lock.json @@ -0,0 +1,61 @@ +{ + "$schema": "internal://schemas/plugin.lock.v1.json", + "pluginId": "gh:EhssanAtassi/angular-marketplace-developer:plugins/angular-performance", + "normalized": { + "repo": null, + "ref": "refs/tags/v20251128.0", + "commit": "80c7e4c3d1c6775c7308e13aeb375ee1d0ebd55b", + "treeHash": "cd816ab8c39027f0c39b501b788e30d10833fab6978113ffaa9df639def40645", + "generatedAt": "2025-11-28T10:10:28.483002Z", + "toolVersion": "publish_plugins.py@0.2.0" + }, + "origin": { + "remote": "git@github.com:zhongweili/42plugin-data.git", + "branch": "master", + "commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390", + "repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data" + }, + "manifest": { + "name": "angular-performance", + "description": "Performance optimization with OnPush, bundle analysis, and change detection strategies", + "version": "1.0.0" + }, + "content": { + "files": [ + { + "path": "README.md", + "sha256": "07ffb40f3698502afd9ed435ac898b390f6a0e9d8e01869117e22d6f2dd3f795" + }, + { + "path": "agents/angular-performance-optimizer.md", + "sha256": "c7eecdd27fcb342db3f0b5f3c5d0c01353fb2909522c7445511b742226384406" + }, + { + "path": ".claude-plugin/plugin.json", + "sha256": "58a8fbef06bc54e05cffd8503b1651f8c161942880f379ec5b1583d14812dd2a" + }, + { + "path": "commands/analyze-bundle.md", + "sha256": "3ad9cf4b0f7f4f413b27b3e979da87156fea17a9b4e7f319132df8aa430ec2cd" + }, + { + "path": "commands/optimize-component.md", + "sha256": "be29cc07a597067dcd98f8ca4e0cc6002cd5745117c94635d2d57b93f4b9a814" + }, + { + "path": "skills/change-detection/SKILL.md", + "sha256": "2ef4cec19aaf1af04693c91cb67b8cfe6f100ee8920e16aa27f6b6f075d43dbc" + }, + { + "path": "skills/bundle-optimization/SKILL.md", + "sha256": "6ad0c52b27ed332f46d60aa4fa947fdc18386079cbe67b9606c1318d001c475d" + } + ], + "dirSha256": "cd816ab8c39027f0c39b501b788e30d10833fab6978113ffaa9df639def40645" + }, + "security": { + "scannedAt": null, + "scannerVersion": null, + "flags": [] + } +} \ No newline at end of file diff --git a/skills/bundle-optimization/SKILL.md b/skills/bundle-optimization/SKILL.md new file mode 100644 index 0000000..d55a764 --- /dev/null +++ b/skills/bundle-optimization/SKILL.md @@ -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//stats.json +``` + +### Analyze with Tools + +```bash +# Webpack Bundle Analyzer +npm install --save-dev webpack-bundle-analyzer +npx webpack-bundle-analyzer dist//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//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: ` + + ` +}) +export class DashboardComponent { + showChart = false; +} + +// After: Dynamic import +@Component({ + template: ` + + + + ` +}) +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): Observable { + // 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: ` + + ` +}) +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 + +# 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 + + + + + +``` + +**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 + + + + + Description + +``` + +### Responsive Images + +```html +Description +``` + +### Lazy Loading + +```html + +Description + + +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 + +# 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! ⚡📦* diff --git a/skills/change-detection/SKILL.md b/skills/change-detection/SKILL.md new file mode 100644 index 0000000..c0f7676 --- /dev/null +++ b/skills/change-detection/SKILL.md @@ -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: ` +
{{ currentTime }}
+ + ` +}) +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: ` +
{{ currentTime }}
+ + ` +}) +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) { + this.products = this.products.map(p => + p.id === id ? { ...p, ...updates } : p + ); + } +} +``` + +### Pattern 2: Async Pipe + +```typescript +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + +
+ {{ user.name }} +
+ ` +}) +export class UserListComponent { + users$ = this.userService.getUsers(); + + constructor(private userService: UserService) {} +} +``` + +### Pattern 3: Event Handlers + +```typescript +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + + ` +}) +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: ` +
{{ count() }}
+ + ` +}) +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: ` +

Cart ({{ itemCount() }} items)

+
Total: {{ total() | currency }}
+ +
+ {{ item.name }} - {{ item.price | currency }} + +
+ ` +}) +export class CartComponent { + items = signal([]); + + 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(null); + user = signal(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: `
...
` +}) +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 + +``` + +--- + +## 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! 🚀*