8.1 KiB
8.1 KiB
Optimize Component Command
Analyze and optimize Angular component performance using OnPush, trackBy, memoization, and other strategies.
Usage
/angular-performance:optimize-component <ComponentName>
Natural Language
- "Optimize UserListComponent performance"
- "Make my dashboard component faster"
- "Reduce change detection in ProductCardComponent"
Optimization Strategies
1. OnPush Change Detection
// 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
// Before
<div *ngFor="let product of products">
{{ product.name }}
</div>
// After
@for (product of products; track product.id) {
<div>{{ product.name }}</div>
}
// Or with trackBy function
trackById(index: number, item: Product): string {
return item.id;
}
<div *ngFor="let product of products; trackBy: trackById">
{{ product.name }}
</div>
Impact: Prevents unnecessary DOM recreations, 60% faster re-renders
3. Memoization with Signals
// 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
// 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
// Before: Renders all items
<div *ngFor="let user of users">
<app-user-card [user]="user" />
</div>
// After: Renders only visible items
<cdk-virtual-scroll-viewport itemSize="80" class="h-screen">
<app-user-card
*cdkVirtualFor="let user of users; trackBy: trackById"
[user]="user"
/>
</cdk-virtual-scroll-viewport>
Impact: 95% faster rendering for large lists (>1000 items)
6. Lazy Loading Images
// Before
<img [src]="product.imageUrl" [alt]="product.name">
// After
<img
[src]="product.imageUrl"
[alt]="product.name"
loading="lazy"
[width]="300"
[height]="200"
>
7. Subscription Management
// 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
// 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
// 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<User[]>([]);
// ✅ 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));
}
}
<!-- Template optimizations -->
<cdk-virtual-scroll-viewport itemSize="60" class="h-screen">
<!-- ✅ 6. Virtual scrolling -->
<div *cdkVirtualFor="let user of users(); trackBy: trackById">
<!-- ✅ 7. Lazy loading images -->
<img
[src]="user.avatar"
loading="lazy"
[width]="50"
[height]="50"
>
<!-- ✅ 8. Async pipe for observables -->
<span>{{ user.name }}</span>
</div>
</cdk-virtual-scroll-viewport>
Performance Metrics
Measure with Chrome DevTools:
// 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
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<input [(ngModel)]="searchTerm" placeholder="Search...">
<cdk-virtual-scroll-viewport itemSize="60">
<div *cdkVirtualFor="let item of filteredItems(); trackBy: trackById">
{{ item.name }}
</div>
</cdk-virtual-scroll-viewport>
`
})
export class FilterableListComponent {
items = signal<Item[]>([]);
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
// Parent: Smart component with OnPush
@Component({
selector: 'app-user-dashboard',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<app-user-profile [user]="user()" />
<app-user-stats [stats]="stats()" />
`
})
export class UserDashboardComponent {
user = signal<User | null>(null);
stats = signal<Stats | null>(null);
}
// Child: Dumb component with OnPush
@Component({
selector: 'app-user-profile',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<h2>{{ user.name }}</h2>
<img [src]="user.avatar" loading="lazy">
`
})
export class UserProfileComponent {
@Input({ required: true }) user!: User;
}
Optimize smart, measure always! 🚀