Knockout.js Memory Leak

A small demonstration of how nodes are being detached from the DOM but not being deleted and thus freeing up memory.

HTML

<script src="http://ajax.aspnetcdn.com/ajax/knockout/knockout-2.1.0.debug.js"></script>
<p><strong>keep making a new KO app</strong><br><button id="smallBtn">let it leak</button><button id="largeBtn">break the dam</button></p>
<p><strong>use the same KO app</strong><br><button id="smallBtn2">let it leak</button><button id="largeBtn2">break the dam</button></p>
<p>There is something that is detaching nodes from the DOM, but not deleting them. If you use Chrome and go to the Dev Tools you can use the Timeline and the Memory graph to see how each time you click a button the node count increases. On applications that update lists and use several foreach loops this can be a severe problem.</p>
<p>If you go to the profiles tab and take a snapshot after hitting a button a few times and switch to the Containment view you can see all of the detached DOM nodes.</p>
<div id="appDiv">
<ul data-bind="foreach: dataList">
    <li data-bind="html: listText"></li>
</ul>
</div>

CSS

strong {
    font-weight: bold;
}
p {
    margin-top: 5px;
    margin-bottom: 5px;
}
#appDiv {
    margin-top: 10px;
}
li span {
    /* 
     * The span elements are only to show significant jumps in node count
     * They don't need to be seen.
     */
    display: none;
}

JavaScript

var listItem = function(i, size) {
    var that = this;
    this.i = i;
    this.size = size;
    this.listText = ko.computed(function() {
        var tmp = "";
        for (var x = 0; x < that.size; x++) {
            tmp += "<span>"+String(x)+"</span>";
        }
        return that.i + " " + tmp;
    });
    
    return this;
};
var koApp = function(iMax, size) {
    var that = this;
    this.iMax = iMax;
    this.size = size;
    this.dataList = ko.observableArray([]);

    this.build = function() {
        for (var x = 0; x < that.iMax; x++) {
            that.dataList.push(new listItem(x, that.size));
        }
    };
    this.clean = function() {
        that.dataList.removeAll();
    };
    this.build();
    return this;
};
$(document).ready(function() {
    var app = null;
    $("#smallBtn").on("click", function() {
        if(app != null)
        {
            app.clean();
            $("#appDiv ul").append('<li data-bind="html: listText"></li>');
        }
        app = new koApp(100, 50);
        ko.cleanNode(document.getElementById("appDiv"));
        ko.applyBindings( app, document.getElementById("appDiv") );
    });
    $("#largeBtn").on("click", function() {
        if(app != null)
        {
            app.clean();
            ko.cleanNode(document.getElementById("appDiv"));
            $("#appDiv ul").append('<li data-bind="html: listText"></li>');
        }
        app = new koApp(500, 100);
        o.cleanNode(document.getElementById("appDiv"));
        ko.applyBindings( app, document.getElementById("appDiv") );
    });
    $("#smallBtn2").on("click", function() {
        if(app != null)
        {
            .clean()app;
            app.build();
            return;
        }
        app = new koApp(100, 50);
        ko.cleanNode(document.getElementById("appDiv"));
        ko.applyBindings( app, document.getElementById("appDiv") );
    });
    $("#largeBtn2").on("click", function() {
        if(app != null)
        {
            app.clean();
 ...