10 Memory Leaks in Web Apps: Causes, Consequences, and Solutions

10 Small Mistakes, which Can Snowball into Major Performance Issues — And How to Stop Them

Prince Singh
5 min readAug 25, 2024

Memory management is a critical aspect of web application development. A well-optimized app runs smoothly, providing a seamless user experience, while poorly managed app can suffer from performance degradation, unresponsive UI, and even crashes. One of the common culprits behind such issues is memory leaks.

In this article, we’ll explore what memory leaks are, why they occur, their consequences, and how to prevent them with real-world examples.

What Are Memory Leaks?

A memory leak occurs when a program allocates memory but fails to release it after it's no longer needed. Over time, these unreleased memory blocks accumulate, leading to reduced available memory, decreased performance, and potentially causing the application to crash.

In web applications, memory leaks are particularly problematic because they can degrade the user experience as the app remains open for extended periods.

Common Causes of Memory Leaks in Web Applications

1. Unsubscribed Observables

In JavaScript frameworks like Angular, RxJS Observables are commonly used for handling asynchronous data streams. However, if subscriptions to these Observables are not properly managed and unsubscribed, they can cause memory leaks.

Example:

export class FeatureComponent implements OnInit, OnDestroy {
private subscription: Subscription;

ngOnInit() {
this.subscription = this.featureService.getData().subscribe(data => {
// handle data
});
}

ngOnDestroy() {
this.subscription.unsubscribe(); // Prevents memory leak
}
}

2. Event Listeners Not Removed

Event listeners that are added but not removed when no longer needed can also lead to memory leaks. This is because the event listener keeps a reference to the DOM element and the associated callback function.

Example:

export class FeatureComponent implements OnInit, OnDestroy {
handleClick = () => { /* handle click */ };

ngOnInit() {
document.addEventListener('click', this.handleClick);
}

ngOnDestroy() {
// Cleans up listener
document.removeEventListener('click', this.handleClick);
}
}

3. Detached DOM Nodes

A common memory leak scenario occurs when DOM nodes are removed from the document but are still referenced by JavaScript code. This prevents the garbage collector from reclaiming the memory.

Example:

export class FeatureComponent {
element: HTMLElement;

ngOnInit() {
this.element = document.getElementById('main-element');
}

removeElement() {
document.body.removeChild(this.element);
this.element = null; // Clear reference to avoid leak
}
}

4. Improper Use of setInterval

Using setInterval without clearing it can lead to a memory leak because the interval continues to execute, keeping a reference to the callback function and any variables it uses.

Example:

export class FeatureComponent implements OnInit, OnDestroy {
intervalId: number;

ngOnInit() {
this.intervalId = setInterval(() => {
// Repeated task
}, 1000);
}

ngOnDestroy() {
clearInterval(this.intervalId); // Clear interval on destroy
}
}

5. Circular References

Circular references occur when two or more objects reference each other, preventing the garbage collector from freeing their memory.

Example:

export class A {
reference: B;
}

export class B {
reference: A;
}

// Ensure to break the references when they are no longer needed
let a = new A();
let b = new B();
a.reference = b;
b.reference = a;

a.reference = null;
b.reference = null; // Breaks the circular reference

6. Global Variables and Closures

Global variables and closures that capture large objects or data can cause memory leaks if not handled properly. Since global variables persist for the lifetime of the page, they can retain more memory than needed.

Example:

export class FeatureComponent {
private data: any;

constructor() {
this.data = largeDataSet; // Captures large data
}

clearData() {
this.data = null; // Clears reference
}
}

7. Caching Without Clearing

Caching data is a common practice for improving performance, but if the cache is not properly managed and cleared when no longer needed, it can lead to memory leaks.

Example:

@Injectable()
export class CacheService {
private cache = new Map<string, any>();

set(key: string, value: any) {
this.cache.set(key, value);
}

clear() {
this.cache.clear(); // Clears cache to prevent leaks
}
}

8. Unoptimized Data Binding

Binding large datasets directly to the template can cause memory leaks if not done efficiently, as the browser may struggle to handle the rendering and data management.

Example:

<!-- Inefficient: Binding large data set directly -->
<div *ngFor="let item of largeDataSet">{{ item }}</div>

<!-- Efficient: Use pagination or virtual scrolling -->
<cdk-virtual-scroll-viewport itemSize="50">
<div *cdkVirtualFor="let item of largeDataSet">{{ item }}</div>
</cdk-virtual-scroll-viewport>

9. Improper Use of ngOnChanges

Performing heavy operations inside ngOnChanges can lead to memory leaks, especially if the component is frequently updated.

Example:

export class FeatureComponent implements OnChanges {
@Input() data: any;

ngOnChanges(changes: SimpleChanges) {
if (changes['data']) {
// Avoid heavy operations here
}
}
}

10. Memory Leaks in Testing

Memory leaks can also occur in tests if components or services are not properly cleaned up after each test case.

Example:

describe('FeatureComponent', () => {
let component: FeatureComponent;
let fixture: ComponentFixture<FeatureComponent>;

beforeEach(() => {
TestBed.configureTestingModule({
declarations: [FeatureComponent]
});

fixture = TestBed.createComponent(FeatureComponent);
component = fixture.componentInstance;
});

afterEach(() => {
fixture.destroy();
// Ensure proper cleanup
});
});

Consequences of Memory Leaks

Memory leaks can have several adverse effects on web applications:

  • Performance Degradation: As memory usage increases, the app may become slower and less responsive.
  • Increased Resource Consumption: The browser consumes more CPU and memory, leading to poor user experience, especially on devices with limited resources.
  • Application Crashes: In extreme cases, the browser may run out of memory, causing the application to crash.
  • Poor User Experience: A sluggish or unresponsive application can frustrate users and lead to a loss of engagement.

How to Detect and Prevent Memory Leaks

1. Using Browser Developer Tools

Browsers come with developer tools that allow you to monitor memory usage, take heap snapshots, and identify memory leaks. Regularly profiling your application can help catch memory leaks early.

2. Implementing Proper Cleanup

Ensure that all resources, such as event listeners, subscriptions, intervals, and dynamic components, are properly cleaned up when they are no longer needed.

3. Minimizing Global Variables

Avoid using global variables unless necessary. Where possible, use local variables that are automatically cleaned up when the function scope ends.

4. Avoiding Circular References

Carefully design your code to avoid circular references. If circular references are necessary, ensure they are broken when the objects are no longer needed.

5. Efficient Data Management

Use techniques like lazy loading, virtual scrolling, and pagination to handle large datasets efficiently, preventing memory overuse.

In conclusion

Memory leaks can significantly impact the performance and reliability of your web applications.

By understanding the common causes and implementing strategies to prevent them, you can ensure that your application remains fast, are efficient and user-friendly.

Regular profiling and monitoring, combined with best practices in coding, are key to keeping memory leaks at bay.

Enjoyed the article? Let’s connect on LinkedIn at isinghprince for more web dev chats!

--

--

Prince Singh

I specialize in architecting enterprise-scale web applications and intuitive data visualizations from zero. Expertise - TypeScript, Rest APIs, Postgres DB