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); });