I found that using the AngularFireAuthModule
from '@angular/fire/auth';
causes a memory leak that crashes the browser after 20 hours.
Version:
I use the latest version updated today using ncu -u for all packages.
Angular Fire: "@angular/fire": "^5.2.3",
Firebase version: "firebase": "^7.5.0"
,
How to reproduce:
I made a minimum reproducible code on StackBliztz editor
Here is the link to test the bug directly StackBlizt test
Symptom:
You can check yourself that the code does nothing. It just prints hello world. However, the JavaScript memory used by the Angular App increases by 11 kb/s (Chrome Task Manager CRTL+ESC). After 10 hours leaving the browser open, the memory used reaches approx 800 mb (the memory footprint is around twice 1.6 Gb!)
As a result, the browser runs out of memory and the chrome tab crashes.
After further investigation using the memory profiling of chrome under the performance tab, I clearly noticed that the number of listeners increases by 2 every second and so the JS heap increases accordingly.
Code that causes the memory leak:
I found that using the AngularFireAuthModule
module causes the memory leak whether it is injected in a component
constructor or in a service
.
import { Component } from '@angular/core';
import {AngularFireAuth} from '@angular/fire/auth';
import {AngularFirestore} from '@angular/fire/firestore';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'memoryleak';
constructor(public auth: AngularFireAuth){
}
}
Question:
It could be a bug in the implementation of FirebaseAuth and I already open a Github issue, but I am looking for a workaround for this issue. I am desperate for a solution. I don't mind even if the sessions across tabs not synchronized. I don't need that feature. I read somewhere that
if you don't require this functionality, the Firebase V6 modularization efforts will allow you to switch to localStorage which has storage events for detecting changes cross tabs, and possibly will provide you the ability to define your own storage interface.
If that's the only solution, how to implement that?
I just need any solution that stops this unnecessary increase of listener because it slows down the computer and crashes my app. My app needs to run for more than 20 hours so it now unusable due to this issue. I am desperate for a solution.
package-lock.json
to confirm that you are using the right version. If unsure, reinstall the firebase
package.Previous versions of Firebase was polling IndexedDB via Promise chaining, which causes memory leaks, see JavaScript's Promise Leaks Memory
var repeat = function() {
self.poll_ =
goog.Timer.promise(fireauth.storage.IndexedDB.POLLING_DELAY_)
.then(goog.bind(self.sync_, self))
.then(function(keys) {
// If keys modified, call listeners.
if (keys.length > 0) {
goog.array.forEach(
self.storageListeners_,
function(listener) {
listener(keys);
});
}
})
.then(repeat)
.thenCatch(function(error) {
// Do not repeat if cancelled externally.
if (error.message != fireauth.storage.IndexedDB.STOP_ERROR_) {
repeat();
}
});
return self.poll_;
};
repeat();
Fixed in subsequent versions using non-recursive function calls:
var repeat = function() {
self.pollTimerId_ = setTimeout(
function() {
self.poll_ = self.sync_()
.then(function(keys) {
// If keys modified, call listeners.
if (keys.length > 0) {
goog.array.forEach(
self.storageListeners_,
function(listener) {
listener(keys);
});
}
})
.then(function() {
repeat();
})
.thenCatch(function(error) {
if (error.message != fireauth.storage.IndexedDB.STOP_ERROR_) {
repeat();
}
});
},
fireauth.storage.IndexedDB.POLLING_DELAY_);
};
repeat();
Linearly increasing listener count is expected as this is what Firebase is doing to poll IndexedDB. However, listeners will be removed whenever the GC wants to.
Read Issue 576302: Incorrectly showing memory (listeners xhr & load) leak
V8 performs Minor GC periodically, which causes those small drops of the heap size. You can actually see them on the flame chart. The minor GCs however may not collect all the garbage, which obviously happens for listeners.
The toolbar button invokes the Major GC which is able to collect listeners.
DevTools tries to not interfere with the running application, so it does not force GC on its own.
To confirm that detached listeners are garbage collected, I added this snippet to pressure the JS heap, thereby forcing GC to trigger:
var x = ''
setInterval(function () {
for (var i = 0; i < 10000; i++) {
x += 'x'
}
}, 1000)
As you can see, detached listeners are removed periodically when GC is triggered.
Similar stackoverflow questions and GitHub issues regarding listener number and memory leaks:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With