Custom directives in Angular provide a powerful way to enhance UI behavior. However, if not implemented efficiently, they can lead to performance issues like unnecessary DOM manipulations, excessive event listeners, and memory leaks. This guide explores best practices for optimizing custom directives for better performance.
1. Optimize Event Listeners
Event listeners attached to directives can degrade performance if not handled efficiently.
Avoid Multiple Unnecessary Event Listeners
@HostListener('mousemove', ['$event'])
onMouseMove(event: MouseEvent) {
console.log('Mouse moved', event);
}
Problem: This triggers too frequently, causing performance issues.
Solution: Use throttleTime
or debounceTime
Using RxJS can help control event execution frequency.
import { Directive, HostListener } from '@angular/core';
import { Subject } from 'rxjs';
import { throttleTime } from 'rxjs/operators';
@Directive({
selector: '[appOptimize]'
})
export class OptimizeDirective {
private mouseMoveSubject = new Subject<MouseEvent>();
constructor() {
this.mouseMoveSubject.pipe(throttleTime(300)).subscribe(event => {
console.log('Throttled Mouse Move:', event);
});
}
@HostListener('mousemove', ['$event'])
onMouseMove(event: MouseEvent) {
this.mouseMoveSubject.next(event);
}
}
Improvement: Throttles mousemove events to once every 300ms.
2. Use ChangeDetectorRef
Smartly
Angular’s change detection mechanism can slow down performance if directives cause frequent checks.
Avoid Triggers on Every Change
constructor(private cd: ChangeDetectorRef) {}
@HostListener('click')
onClick() {
this.cd.detectChanges(); // Not recommended
}
Problem: Forces a change detection cycle unnecessarily.
Solution: Use markForCheck
for OnPush Strategy
constructor(private cd: ChangeDetectorRef) {}
@HostListener('click')
onClick() {
this.cd.markForCheck();
}
Improvement: Marks only affected components for change detection.
3. Prevent Memory Leaks
Memory leaks occur when directives subscribe to observables but don’t unsubscribe properly.
Avoid Unmanaged Subscriptions
constructor(private service: DataService) {
this.service.getData().subscribe(data => {
console.log(data);
});
}
Problem: The subscription remains even after directive destruction.
Solution: Use ngOnDestroy
to Unsubscribe
import { OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
export class OptimizeDirective implements OnDestroy {
private subscription: Subscription;
constructor(private service: DataService) {
this.subscription = this.service.getData().subscribe(data => {
console.log(data);
});
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}
Improvement: Cleans up resources when the directive is destroyed.
Alternative: Use takeUntil
with a Subject
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
export class OptimizeDirective implements OnDestroy {
private destroy$ = new Subject<void>();
constructor(private service: DataService) {
this.service.getData().pipe(takeUntil(this.destroy$)).subscribe(data => {
console.log(data);
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}
Improvement: Ensures automatic cleanup of multiple subscriptions.
4. Reduce DOM Manipulations
Excessive DOM manipulations can slow down rendering performance.
Avoid Direct DOM Access Repeatedly
@HostListener('click')
onClick() {
document.getElementById('element')!.style.backgroundColor = 'red';
}
Problem: Direct DOM access makes the directive dependent on browser APIs, breaking Angular’s reactivity.
Solution: Use Renderer2
for Better Performance
import { Renderer2, ElementRef } from '@angular/core';
constructor(private el: ElementRef, private renderer: Renderer2) {}
@HostListener('click')
onClick() {
this.renderer.setStyle(this.el.nativeElement, 'background-color', 'red');
}
Improvement: Makes DOM updates optimized and Angular-friendly.
5. Use @Input()
Wisely
Using @Input()
improperly can trigger unnecessary change detections.
Avoid Object Mutations
@Input() data: any;
ngOnChanges() {
this.data.name = 'Updated'; // Changes object reference
}
Problem: Mutating the object causes unnecessary component re-renders.
Solution: Use Immutable Data Structures
@Input() data: any;
ngOnChanges() {
this.data = { ...this.data, name: 'Updated' }; // Creates a new object reference
}
Improvement: Ensures Angular detects changes efficiently.
6. Lazy Load Heavy Directives
If a directive is used in multiple places but not always needed, consider lazy loading it.
Solution: Use NgModule
and loadChildren
@NgModule({
declarations: [HeavyDirective],
exports: [HeavyDirective]
})
export class HeavyDirectiveModule {}
Lazy load the module when needed.
Improvement: Prevents loading unused directives on initial load.
7. Optimize Large Lists
Directives applied to large lists can impact performance.
Avoid Using Directives on Large Lists Without Virtual Scrolling
<div *ngFor="let item of items" appHeavyDirective>{{ item }}</div>
Problem: Applies the directive to each item, causing excessive rendering.
Solution: Use Angular’s Virtual Scrolling (cdkVirtualScrollViewport
)
<cdk-virtual-scroll-viewport itemSize="50" class="example-viewport">
<div *cdkVirtualFor="let item of items" appHeavyDirective>{{ item }}</div>
</cdk-virtual-scroll-viewport>
Improvement: Renders only visible items, improving performance.
8. Optimize Animations in Directives
Avoid CSS animations that cause layout reflows.
Avoid Changing top
, left
, width
, height
this.el.nativeElement.style.left = '10px'; // Causes layout reflow
Problem: Triggers browser reflow, slowing down rendering.
Solution: Use transform: translate()
for Smooth Animations
this.renderer.setStyle(this.el.nativeElement, 'transform', 'translateX(10px)');
Improvement: Uses GPU acceleration, making animations smoother.