I’ve built a sample Angular application using RxJS to simulate various memory leaks. The majority of these techniques apply to any component-based framework using RxJS.

Download the Source Code 🚀

We’ll cover:

Strategies for detecting a memory leak Unsubscribe and Garbage Collection Solutions to resolve subscription-based memory leaks

Strategies For Detecting A Memory Leak

Using Google Chrome Developer Tools 🛠 (Ctrl+Shift+I) In Google Chrome

Allocation Timeline 📈

Allocation Timelines allow us to see if the minimum size of our heap is growing over time. We’ll start with a baseline before creating or destroying any components.

Now while the timeline is running, we’ll create/destroy the LargeLeak Component multiple times. Note, I’ve typed large into the filter as we are looking for allocations of the LargeLeakComponent

LargeLeakComponent.ts

Now we return to our starting point (destroy LargeLeakComponent) → manually run garbage collection (🗑 garbage-can icon in the top left) → and restart the allocation timeline (⚫ circle icon in the top left). We can see our minimum heap size has increased and the allocations are not being released.

Heap Snapshot 📸

The heap snapshot is useful once we’ve identified a leak. Unlike allocation timelines 🐌, a heap snapshot won’t cause latency while we interact with our app. Ultimately, both snapshot and allocation timeline can be used to detect leaks.

We’ll start with a heap snapshot before doing anything in our application. This will be our baseline or starting point. We can see 0 ServiceObservableLeak Components have been allocated.

ServiceObservableLeak.ts

After toggling creation/destruction multiple times → manually running garbage collection 🗑→ and taking another snapshot ⚫. We can see that the component is not being released via garbage collection.

Note, searching for just the component is not always an effective method for finding a memory leak. If we assume there is a memory leak, we should also inspect the number of subscribers in memory. 🔍 Using the snapshot comparison (Dropdown next to filter), we can compare two separate snapshots. This shows us we’ve allocated 30 subscribers and deleted 0. More on this in “Unsubscribe and Garbage Collection”.

Unsubscribe and Garbage Collection

It’s commonly mentioned that subscriptions hold a reference to the component. Thus, the component cannot be released, creating a memory leak. Not being able to release either object referencing each other is known as a cycle. [1]

Cycles are a limitation of the “Reference-counting garbage collection” algorithm. Modern browsers utilize a “Mark-and-sweep” algorithm 🎯. Mark and Sweep algorithms will collect all non-reachable objects from the root (Global Object). This solves the limitations of cycles.

We can validate this idea of cycles being handled by the garbage collector by testing with a local finite observable. As we toggle the local-finite component → manually run garbage collection 🗑→ take a heap snapshot ⚫.

FiniteObservableComponent.ts

We can see that the component is not leaking, even though we have not used a subscription management strategy (such as unsubscribe).

”So I can just let garbage collection manage my subscriptions?”

No.

If the subscription/component still has some reference back to the root, it will not be garbage collected. We can see this in our ServiceObservableLeak Component from the previous example. Since our observable (observable$) is allocated in a service (SourceService), the component still has a reference back to the root via the observable. We need to unsubscribe for the component (ServiceObservableLeakComponent) to be available for garbage collection.

Not managing subscriptions is a bad practice and should be avoided. Take the FromEvent Component for example. The component initializes a local observable that is watching for click events on a button within FromEventComponent.

FromEventComponent.ts

In this example, the component is being released but the subscriber is not. To make the situation more complicated, the subscriber only leaks when the click event has been fired at least once. This creates a memory leak that is very difficult to track down. — The solution is to always manage your subscriptions.

Solutions

I plan to write in more detail on these methods in a future article. For now, here’s a high-level overview. 🌎

Unsubscribe

Unsubscribing to all subscriptions in the ngOnDestroy method is a valid strategy. This is similar to what the async pipe does under the hood on a component-level.

Async Pipe

Will manage your subscription based on the life of the DOM element it’s associated with. Async pipe will make a new subscription wherever it’s declared. Use “as” to re-use the value provided.

I’ve written in detail on the async pipe and how to use it here [2 ]

Take

take, takeUntil, and takeWhile will all manage a subscription automatically based on a condition being met. For example, takeUntil will take an observable as an argument and maintain a subscription until that observable emits a value.