Memory leak due to unclosed event listeners

Loading

A memory leak occurs when an application does not properly release unused memory, causing unnecessary consumption of system resources. One of the most common causes of memory leaks in AngularJS is unclosed event listeners that persist even after a component or scope is destroyed.

In this guide, we will cover:
How event listeners cause memory leaks.
Scenarios where this happens in AngularJS.
Best practices to prevent and fix memory leaks.


1. How Event Listeners Cause Memory Leaks in AngularJS

When you register an event listener (e.g., addEventListener(), $on(), $watch()), it remains in memory unless explicitly removed. If an AngularJS scope is destroyed but the listener remains, it keeps consuming memory and may even cause unexpected behavior.

Example of a Memory Leak

document.addEventListener("click", function () {
console.log("Clicked!");
});

Issue: If this event listener is added inside an AngularJS controller, it remains even after navigating away.


2. Common Scenarios Leading to Memory Leaks

Here are some common ways unclosed event listeners cause memory leaks:

Event Listeners on document or window

Bad Example

app.controller("MyController", function ($scope) {
document.addEventListener("mousemove", function () {
console.log("Mouse moved!");
});
});

Problem:

  • This event listener stays even after the controller is destroyed.
  • Memory usage keeps growing as new listeners keep adding without removing the old ones.

Solution: Remove Listener on $destroy

app.controller("MyController", function ($scope) {
function onMouseMove() {
console.log("Mouse moved!");
}
document.addEventListener("mousemove", onMouseMove);

$scope.$on("$destroy", function () {
document.removeEventListener("mousemove", onMouseMove);
});
});

Now, when the controller is destroyed, the event listener is also removed.


$on() Event Listeners Not Removed

Bad Example

app.controller("MyController", function ($scope) {
$scope.$on("customEvent", function () {
console.log("Custom event triggered!");
});
});

Problem:

  • If $scope.$on() is used without $destroy, the event listener remains even after the scope is removed, leading to memory leaks.

Solution: Unregister the Listener

app.controller("MyController", function ($scope) {
var eventListener = $scope.$on("customEvent", function () {
console.log("Custom event triggered!");
});

$scope.$on("$destroy", function () {
eventListener(); // Unregisters the event
});
});

$destroy ensures that when the scope is removed, so is the event listener.


$watch() Keeping References in Memory

Bad Example

app.controller("MyController", function ($scope) {
$scope.$watch("user.name", function (newValue) {
console.log("User name changed:", newValue);
});
});

Problem:

  • If $watch() remains active even when the scope is destroyed, AngularJS continues tracking changes in memory.

Solution: Remove Watchers on $destroy

app.controller("MyController", function ($scope) {
var unwatch = $scope.$watch("user.name", function (newValue) {
console.log("User name changed:", newValue);
});

$scope.$on("$destroy", function () {
unwatch(); // Unregister the watcher
});
});

This prevents AngularJS from tracking unnecessary changes after the controller is destroyed.


Listeners Attached in Directives

Bad Example

app.directive("customDirective", function () {
return {
restrict: "A",
link: function (scope, element) {
element.on("click", function () {
console.log("Element clicked!");
});
}
};
});

Problem:

  • The click listener remains even after the directive is removed.

Solution: Clean Up in $destroy

app.directive("customDirective", function () {
return {
restrict: "A",
link: function (scope, element) {
function handleClick() {
console.log("Element clicked!");
}
element.on("click", handleClick);

scope.$on("$destroy", function () {
element.off("click", handleClick);
});
}
};
});

element.off() ensures that the event listener is properly removed when the directive is destroyed.


3. Best Practices to Prevent Memory Leaks

Always Remove Event Listeners

  • Use $destroy to remove listeners from $on, $watch, and native event listeners.
  • Example: $scope.$on("$destroy", function () { document.removeEventListener("click", onClick); });

Use Weak References When Possible

  • Avoid holding long-lived references in memory.
  • Example: let ref = new WeakMap();

Use $timeout Instead of setTimeout

  • setTimeout() may execute after the scope is destroyed, causing errors.
  • Use $timeout so Angular cleans up automatically: var timer = $timeout(function () { console.log("Timeout executed!"); }, 2000); $scope.$on("$destroy", function () { $timeout.cancel(timer); });

Use $interval Instead of setInterval

  • setInterval() keeps running even if the scope is destroyed.
  • Use $interval to ensure it gets removed: var interval = $interval(function () { console.log("Interval running..."); }, 1000); $scope.$on("$destroy", function () { $interval.cancel(interval); });

Leave a Reply

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