Performance optimization of custom directives

Loading

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.

Leave a Reply

Your email address will not be published. Required fields are marked *