Advanced Techniques for Memory Management in Angular Applications

What are memory leaks, and how do we deal with them?

Francois J Rossouw
DVT Software Engineering
11 min readNov 1, 2023

--

The Artist can be found at the following link

Memory Management plays a crucial role in developing efficient and high-performing Angular Applications. Angular applications are becoming increasingly more complex and feature-rich, and managing memory effectively is becoming more critical. Unused memory can lead to memory leaks if not properly released; thus, a decrease in performance, an increase in resource usage, and, worst case, even application crashes can occur.

It is crucial to understand the basics and implementation of advanced memory management techniques to build robust and optimised Angular applications ranging from small to enterprise scales.

This article aims to delve into the various causes of memory leaks and demonstrate various advanced techniques in easy-to-understand terms that will assist any developer when they are trying to overcome memory leaks.

The techniques that are being demonstrated will also assist in the prevention and optimisation of memory use within any Angular application.

The following techniques will be demonstrated: the async pipe, leveraging the ngOnDestroyAngular life-cycle hook, the Renderer2 service, as well as implementing various custom memory management strategies.

Performance Profiling and Code Optimisation will also be discussed, which can essentially be just as important and assist in the resolution of memory-related issues.

By implementing these advanced techniques, any Angular application can run efficiently, with optimised memory usage, and provide a higher-standard user experience.

Understanding Memory Leaks

A challenge common to Angular, known to most developing Angular applications, is the dreaded memory leaks. They often lead to unexpected behaviour as well as performance issues. As developers, we need to understand the various causes of memory leaks. Below are key concepts accompanied by code examples that will help you better understand memory leaks in Angular applications.

Advanced Memory Management Techniques

This section will discuss the advanced techniques that can be employed to improve and manage memory leaks, such as:

A. Unsubscribing from Observables,
B. Implementing proper event listener management,
C. Implementing Custom Memory Management Strategies,
D. Avoiding the use of Global Variables, and
E. Keeping the Document Object Model (DOM) Clean.

A. Unsubscribing from Observables

When handling asynchronous data streams, we tend to think of Observables, which are one of the main causes of memory leaks if not properly unsubscribed. Three approaches to unsubscribing are being discussed in this article, and they are as follows:

Approach 1: Using the async pipe

This approach leverages a native feature within Angular that automatically unsubscribes from Observables when the component is destroyed. This feature is called the async pipe. It is a simple yet effective process for managing subscriptions in components.

Here is an example of how to use the async pipe:

import { Component } from '@angular/core';
import { Observable } from 'rxjs';
import { DataService } from 'src/app/services/data.service';
import { iData } from 'src/app/models/data';

@Component({
selector: 'app-unsubscribing-from-observables-async-pipe',
styleUrls: ['./unsubscribing-from-observables-async-pipe.component.scss'],
template: `
<h2>Unsubscribing from Observables using the Async Pipe</h2>
<div *ngIf="data$ | async as data">
<div *ngFor="let item of data">{{item.name}}</div>
</div>
`
})
export class UnsubscribingFromObservablesAsyncPipeComponent {
data$: Observable<iData[]>;

constructor(private dataService: DataService) {
this.data$ = this.dataService.getData();
}
}

Approach 2: Using thengOnDestroy life-cycle hook

Another built-in feature of Angular is its life-cycle hooks. By utilising the ngOnDestroy life-cycle hook, you can manually unsubscribe from Observables.

Here is an example of how to use the ngOnDestroy life-cycle hook:

import { Component, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { iData } from 'src/app/models/data';
import { DataService } from 'src/app/services/data.service';

@Component({
selector: 'app-unsubscribing-from-observables-on-ng-destroy',
styleUrls: ['./unsubscribing-from-observables-on-ng-destroy.component.scss'],
template: `
<h2>Unsubscribing from Observables using OnNgDestroy</h2>
<div *ngIf="data">
<div *ngFor="let item of data">{{item.name}}</div>
</div>
`
})
export class UnsubscribingFromObservablesOnNgDestroyComponent implements OnDestroy {
private subscription$: Subscription;
data: iData[];

constructor(private dataService: DataService) {
this.subscription$ = this.dataService.getData()
.subscribe((data: iData[]) => {
this.data = data;
});
}

ngOnDestroy(): void {
this.subscription$.unsubscribe();
}
}

The example above uses a variable called subscription$ to keep track of a single data subscription. This variable is then manually unsubscribed by using the ngOnDestroy life-cycle hook.

Approach 3: Using the push pipe, also known as the ngrxPush pipe

To utilise this approach, you must install an additional library in your application. This library is called @ngrx/component, and it is installed by using the following command: npm install @ngrx/component.

This is widely seen as an alternative to the well-known async pipe. Apart from looking similar on their template level, there are significant differences, where a key difference is that the ngrxPush pipe brings corrections forward on change detection control.

Both the async pipe and the ngrxPush pipe work as follows: a new value is passed through the data stream called an Observable, and the value then updates the variable being passed to the view. After this has been completed, it informs the markForCheck method to inform the change detection mechanism system that the user view and template need to be re-rendered.

Thus, the Push Pipe is a drop-in replacement for the async pipe. It triggers change detection in an Angular zone-less context, or it triggers the markForCheck like the async pipe in a zone context.

import { Component } from '@angular/core';
import { Observable } from 'rxjs';
import { DataService } from 'src/app/services/data.service';
import { iData } from 'src/app/models/data';

@Component({
selector: 'app-unsubscribing-from-observables-async-pipe',
styleUrls: ['./unsubscribing-from-observables-async-pipe.component.scss'],
template: `
<h2>Unsubscribing from Observables using the Push Pipe</h2>
<div *ngIf="data$ | async as data">
<div *ngFor="let item of data">{{item.name}}</div>
</div>
`
})
export class UnsubscribingFromObservablesAsyncPipeComponent {

data$: Observable<iData[]>;

constructor(private dataService: DataService) {
this.data$ = this.dataService.getData();
}

}

Other key differences between the async pipe and the ngrxPush (push) pipe to note are as follows:

  • The ngrxPush pipe will not mark the host component as dirty when an observable emits various similar (exact) values in a row.
  • The ngrxPush pipe will not mark the host component as dirty when an observable emits values synchronously.
  • The ngrxPush pipe will trigger change detection when an observable emits a new value in zone-less mode.

B. Implementing Proper Event Listener Management

An often overlooked cause of memory leaks is event listeners. To manage these, you can make use of an Angular service called Renderer2. This service is used to register and unregister event listeners, which, if managed properly, prevents memory leaks.

Here is an example of how to use Renderer2 to register and unregister an event listener:

import { Component, ElementRef, Renderer2 } from '@angular/core';

@Component({
selector: 'app-unregister-event-listeners',
styleUrls: ['./unregister-event-listeners.component.scss'],
template: `
<div>Events Triggered {{triggerCount}}</div>
<button type="button" (myClick)="$event">Trigger Click Event</button>
`
})
export class UnregisterEventListenersComponent {
// We define a function to store our event listener
private clickListener: () => void;
triggerCount: number = 0;

constructor(
private renderer: Renderer2,
private el: ElementRef
) { }

ngAfterViewInit() {
this.clickListener = this.renderer.listen(
this.el.nativeElement,
'click',
() => {
this.triggerCount++;
}
);
}

ngOnDestroy() {
this.clickListener();
}
}

In the example, the Renderer2 service is used to register a click event listener on the component’s native element. This is done in the ngAfterViewInit life-cycle hook. The event listener is then unregistered using the function returned by the renderer.listen in the ngOnDestroy life-cycle hook, preventing memory leaks.

C. Implementing Custom Memory Management Strategies

Custom memory management strategies are sometimes required and can easily be implemented. Below are two examples of custom memory management strategies:

Example 1: Manual Subscription Management

Manually subscribing in components is possible, but you need to make sure that all subscriptions are dealt with appropriately. This is easily done by keeping track of all the subscriptions either in an array or by adding them all to one subscription.

Below is an example of adding all subscriptions on one subscription:

import { Component, OnDestroy } from '@angular/core';
import { iData } from 'src/app/models/data';
import { Subscription } from 'rxjs';
import { DataService } from 'src/app/services/data.service';

@Component({
selector: 'app-manual-subscription-management',
styleUrls: ['./manual-subscription-management.component.scss'],
template: `
<h2>Manual Subscription Management</h2>
<div *ngIf="data">
<div *ngFor="let item of data">{{item.name}}</div>
</div>`
})
export class ManualSubscriptionManagementComponent implements OnDestroy {
private subscription: Subscription;
data: iData[];

constructor(private dataService: DataService) {
this.subscription.add(
this.dataService.getData()
.subscribe((data: iData[]) => {
this.data = data;
})
);
}

ngOnDestroy(): void {
this.subscription.unsubscribe();
}
}

A subscription is used to keep track of all the subscriptions called subscription. The life-cycle hook ngOnDestroy is then used to manually unsubscribe from all the subscriptions as soon as the component is destroyed.

Example 2: Using the takeUntil Operator

An Angular library called RxJS has an operator that can automatically unsubscribe from Observables when certain conditions are met. This is called the takeUntil and you can see an example of a condition being set below.

import { Component, OnDestroy  } from '@angular/core';
import { Subject } from 'rxjs';
import { DataService } from 'src/app/services/data.service';
import { takeUntil } from 'rxjs/operators';
import { iData } from 'src/app/models/data';

@Component({
selector: 'app-rxjs-take-until',
styleUrls: ['./rxjs-take-until.component.scss'],
template: `
<h2>Rxjs Take Until</h2>
<div *ngIf="data">
<div *ngFor="let item of data">{{item.name}}</div>
</div>
`
})
export class RxjsTakeUntilComponent implements OnDestroy {
private ngUnsubscribe$ = new Subject<void>();
data: iData[];

constructor(private dataService: DataService) {
this.dataService.getData()
.pipe(
takeUntil(this.ngUnsubscribe$)
)
.subscribe((data: iData[]) => {
this.data = data;
})
}

ngOnDestroy(): void {
this.ngUnsubscribe$.next();
this.ngUnsubscribe$.complete();
}

}

A local subject variable is created called ngUnsubscribe$, and it is used to emit a value when the component is destroyed. When the value is emitted it triggers the takeUntil operator to automatically unsubscribe from the Observable getData.

D. Avoid Using Global Variables

Angular applications can also get memory leaks when global variables are introduced. It is vital to avoid storing data in these variables due to them not being properly cleaned or taken care of, especially when components are destroyed. This is due to global variables being persisted in memory.

Instead, a local component-level variable or service should be used to store and manage data within the Angular component tree.

If you absolutely need to use a global variable, take note of the following best practices to manage them, as seen below:

  • Limit the use of global variables:
    One of the best practices to follow when developing Angular applications is limiting or avoiding the use of global variables. Data and functionality should be encapsulated as much as possible in components or services with isolated scopes.
  • Properly initialise and clean up global variables:
    Make sure that global variables are initialised to an appropriate value wherever they are being used. Also, make sure that you clean these variables once they are not being used or once components are being destroyed. This involves setting the variable to either null or undefined and, in some cases, removing all references thereof.
  • Use Angular services for shared data:
    Services are one of the most recommended alternatives to sharing data and functionality across components. By using services, you eliminate the use of global variables. Services encapsulate data and functionality in a controlled manner and can, therefore, be injected into various components as required. Take note: as soon as the service is no longer required, you must clean data and all references stored within the service.
  • Use Angular’s dependency injection:
    By utilising Angular’s Dependency Injection, you can manage dependencies between components and services centrally. This is useful because it takes care of the creation and destruction of instances of services, ensuring the proper cleanup of resources. It, therefore, ensures data and functionality sharing between components by avoiding global variables.
  • Make use of Angular’s life-cycle hooks:
    Every component in Angular makes use of life-cycle hooks. Some of the more well-known hooks are ngOnInit, ngOnDestroy, etc., which are dedicated to performing various tasks. These hooks can be utilised to properly initialise and clean up data and references within components, thus eliminating the use of global variables.
  • Avoid Circular Referencing:
    Circular references do cause memory leaks because they create a reference cycle, which in turn prevents garbage collection. Therefore, you need to take care and make sure circular references are avoided between components, services or any other objects within your Angular application.

E. Keeping the DOM Clean

Keeping the Angular DOM clean is essential because, more often than not, the DOM is manipulated by Angular. Components need to be destroyed when they are no longer required.

An example of this would be to create DOM elements in a component dynamically. To keep the DOM clean, you should remove these elements from the DOM when the component is destroyed. This also includes references to these elements.

Performance Profiling and Optimisation

Memory leak management also includes profiling and optimising your application. This can be done by utilising various extensions and tools within internet browsers.

Some popular performance profiling tools in Angular include:

  • Angular DevTools:
    Angular DevTools is a popular internet browser extension for Google Chrome and Firefox. It provides developers with a set of specifically designed debugging and profiling tools for Angular. This allows developers to inspect component trees, view states for various components, track changes within the application and profiling performance.
  • Chrome DevTools
    Google Chrome also offers its own built-in set of tools for profiling the performance of Angular applications. They provide developers with features for memory profiling, CPU profiling, as well as timeline recording. These all assist in the identification of memory-related performance issues.
  • Augury Internet Browser Extension:
    Another popular extension used when debugging Angular applications is Augury. It provides a visual representation of the component tree, displaying component states and props, while it also offers performance profiling features such as memory profiling and CPU profiling.

Various additional Optimisation Techniques:

Once the performance issues have been identified, the following can be implemented to optimise your Angular application:

  1. Unsubscribe from unnecessary Observables in components or services when they are no longer required.
  2. Make use of Angular’s Change Detection Strategy called onPush. It is triggered when the properties of inputs change or when an event is triggered.
  3. Lazy Load modules only when they are required. This assists in decreasing the load times of the application whilst optimising memory usage.
  4. Utilise Angular’s built-in library Renderer2. This service optimises and efficiently and directly manipulates the DOM, reducing performance issues.
  5. trackBy should be used when collections are iterated on the DOM. This provides a function that returns a unique identifier for each item on the DOM. This assists in optimising the rendering of the list and reduces unnecessary updates to the DOM in Angular.

Conclusion

A critical aspect of building high-performance and reliable Angular applications is managing memory effectively.

In this article, we explored various advanced techniques to manage memory, including using pipes and life-cycle hooks. The importance of performance profiling and optimisation to help identify memory-related issues was also discussed.

In conclusion, proactively managing memory and optimising performance is vital for delivering fast, efficient, and reliable Angular Applications. By understanding and implementing the various techniques discussed in this article, developers can create web experiences that excel in memory usage and performance.

Stay mindful of memory management best practices and continuously monitor and optimise the performance of your Angular applications to provide an exceptional user experience.

Thank you for reading my article. I hope you learned something new and exciting and now have a better understanding of how memory leaks work and how to manage and prevent them.

Please feel free to leave a comment if you need clarification or if you have something to add. You can also find a complete GitHub repository for this topic below:

If you enjoyed this article, please check out the DVT Engineering space for more interesting articles and follow me for future topics.

Additional Resources:

  1. Angular Documentation
  2. NgZone
  3. Detecting and Fixing Memory Leaks with Chrome Dev Tools
  4. Lazy Loading Modules
  5. Component Lifecycle
  6. Angular DevTools
  7. Augury Chrome Extension

--

--