Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Memory leak when using angular's $compile with a new scope

I want to dynamically create angular components using javascript, and then have angular compile them using $compile with a newly created scope. Then when I have no longer use for that component, I want to destroy the component and the new scope.

Everything works as expected, except from the fact that even though I am destroying the new scope, all the memory that it uses is never released.

Here is part of a simplified version of that code:

app.controller("mainCtrl", ["$scope", "$compile", function($scope, $compile) {
    var childScope;

    //call this every time the button is clicked
    this.createDirective = function() {
        //dynamically create a new instance of the custom directive
        var customDirective = document.createElement("custom-directive");

        //if another child scope exists, destroy it
        if (childScope) {
            childScope.$destroy();
            childScope = undefined;
        }

        //create a new child scope
        childScope = $scope.$new();

        //compile the custom directive
        $compile(customDirective)(childScope);
    };

}]);

Full working example of this code is here

All this code does, is create a new component every time the button is clicked, but first destroy any component that already exists. Notice that I am not actually adding the compiled component in the page, because I noticed that the leak was still there regardless of whether I used it or not.

Using Chrome's development tools (Profiles -> Record Allocation Timeline -> Start) I see the following memory usage after clicking the button a few times:

Memory consumption

It is clear that any memory occupied by the customDirective is never actually released, even though the scope's $destroy function is being called.

I have successfully used $compile in the past without creating a new scope, but it seems that I am missing something in this scenario. Should I be doing something else as well to make sure that there are no references to the new scope?

Edit

Based on the answer below by JoelCDoyle, here is the fix (I add an on destroy function to the scopes I create):

app.controller("mainCtrl", ["$scope", "$compile", function($scope, $compile) {
    var childScope;

    //call this every time the button is clicked
    this.createDirective = function() {
        //dynamically create a new instance of the custom directive
        var customDirective = document.createElement("custom-directive");

        //if another child scope exists, destroy it
        if (childScope) {
            childScope.$destroy();
            childScope = undefined;
        }

        //create a new child scope
        childScope = $scope.$new();

        //compile the custom directive
        var compiledElement = $compile(customDirective)(childScope);

        //FIX: remove the angular element
        childScope.$on("$destroy", function() {
            compiledElement.remove();
        });
    };
}]);

Fixed fiddle

like image 279
kapoiosKapou Avatar asked Oct 04 '16 21:10

kapoiosKapou


People also ask

What could be the possible cause of memory leaks?

A memory leak may also happen when an object is stored in memory but cannot be accessed by the running code (i.e. unreachable memory). A memory leak has symptoms similar to a number of other problems and generally can only be diagnosed by a programmer with access to the program's source code.

How would you prevent a memory leak?

To avoid memory leaks, memory allocated on heap should always be freed when no longer needed.

Does function create memory leak?

No it does not cause memory leaks.

Can memory leak happen stack?

Stack memory leaks occur when a method keeps getting called but never exits. This can happen if there is an infinite loop or if the method is being called with different data each time but the data is never used. Eventually, the stack will fill up and the program will run out of memory.


1 Answers

I think I've found a solution to this: https://jsfiddle.net/yqw1dk0w/8/

app.directive('customDirective', function(){
  return {
    template: '<div ng-controller="customDirectiveCtrl"></div>',
    link: function(scope, element) {
      scope.$on('$destroy', function() {
        element.remove();
      });
    }
  };
});

I'm still a little fuzzy on why this works, but this section, How Directives are Compiled, in angular compile docs provides a clue: https://docs.angularjs.org/guide/compiler

$compile links the template with the scope by calling the combined linking function from the previous step. This in turn will call the linking function of the individual directives, registering listeners on the elements and setting up $watchs with the scope as each directive is configured to do.\

The result of this is a live binding between the scope and the DOM. So at this > point, a change in a model on the compiled scope will be reflected in the DOM.

Destroying the scope, I'm guessing, does not remove these element listeners. That's what the above code is doing: destroy directive/child scope on scope destroy

like image 117
Pop-A-Stash Avatar answered Oct 03 '22 07:10

Pop-A-Stash