Knock Me Out

Thoughts, ideas, and discussion about Knockout.js

Cleaning Up After Yourself in Knockout.js

| Comments

Last summer, I had the opportunity to speak at ModernWebConf, ThatConference, and devLink on the topic of browser memory leaks. The talk was focused on the tools and techniques that you can use for memory leak testing and situations in JavaScript that comonly cause these leaks. Slides for the presentation can be found here.

With the rise of single-page applications and increased complexity (and amount) of JavaScript on the client-side, memory leaks are a common occurrence. Knockout.js applications are not immune to these problems. In this post, I will review some scenarios that often contribute to memory leaks and discuss the APIs in Knockout that can be used to prevent and resolve these issues.

The main source of leaks in KO

Memory leaks in KO are typically caused by long-living objects that hold references to things that you expect to be cleaned up. Here are some examples of where this can occur and how to clean up the offending references:

1. Subscriptions to observables that live longer than the subscriber

Suppose that you have an object representing your overall application stored in a variable called myApp and the current view as myViewModel. If the view needs to react to the app’s language observable changing, then you might make a call like:

1
myApp.currentLanguage.subscribe(myCurrentView.languageHandler, myCurrentView);

What this technically does is goes to myApp.currentLanguage and adds to its list of callbacks with a bound function (languageHandler) that references myCurrentView. Whenever myApp.currentLanguage changes, it notifies everyone by executing each registered callback.

This means that if myApp lives for the lifetime of your application, it will keep myCurrentView around as well, even if you are no longer using it. The solution, in this case, is that we need to keep a reference to the subscription and call dispose on it. This will remove the reference from the observable to the subscriber.

1
2
3
4
myCurrentView.languageSubscription = myApp.currentLanguage.subscribe(myCurrentView.languageHandler, myCurrentView);

// somewhere later in the code, when you are disposing of myCurrentView
myCurrentView.languageSubscription.dispose();

2. Computeds that reference long-living observables

1
2
3
myCurrentView.userStatusText = ko.computed(function() {
    return myApp.currentUser() + " (" + myCurrentView.userStatus + ")";
}, myCurrentView)

In this case, the userStatusText computed references myApp.currentUser(). As in the subscription example, this will add to the list of callbacks that myApp.currentUser needs to call when it changes, as the userStatusText computed will need to be updated.

There are a couple of ways to solve this scenario:

  • we can use the dispose method of a computed, like we did with a manual subscription.
1
2
// in disposal code
myCurrentView.userStatusText.dispose();
  • in KO 3.2, a specialized computed called a ko.pureComputed was added (docs here). A pure computed can be created by using ko.pureComputed rather than ko.computed or by pasing the pure: true option when creating a normal computed. A pure computed will automatically go to sleep (release all of its subscriptions) when nobody cares about its value (nobody is subscribed to it). Calling dispose on a pure computed would likely not be necessary for normal cases, where only the UI bindings are interested in the value. This would work well for our scenario where a temporary view needs to reference a long-living observable.

3. Event handlers attached to long-living objects

In custom bindings, you may run into scenarios where you need to attach event handlers to something like the document or window. Perhaps the custom binding needs to react when the browser is resized. The target needs to keep track of its subscribers (like an observable), so this will create a reference from something long-living (document/window in this case) back to your object or element that is bound.

To solve this issue, inside of a custom binding, Knockout provides an API that lets you execute code when the element is removed by Knockout. Typically, this removal happens as part of templating or control-flow bindings (if, ifnot, with, foreach). The API is ko.utils.domNodeDisposal.addDisposeCallback and would be used like:

1
2
3
4
5
6
7
8
9
10
11
12
13
ko.bindingHandlers.myBinding = {
    init: function(element, valueAccessor, allBindings, data, context) {
        var handler = function () {
            // do something with element, valueAccessor, etc.
        };

        $(window).on("resize", handler);

        ko.utils.domNodeDisposal.addDisposeCallback(element, function() {
            $(window).off("resize", handler);
        });
    })
};

If you did not have easy access to the actual handler attached, then you might consider using namespaced events like $(window).on("resize.myPlugin", handler) and then remove the handler with $(window).off("resize.myPlugin").

4. Custom bindings that wrap third-party code

The above issue is also commonly encountered when using custom bindings to wrap third-party plugins/widgets. The widget may not have been designed to work in an environment where it would need to be cleaned up (like a single-page app) or may require something like a destroy API to be called. When choosing to reference third-party code, it is worthwhile to ensure that the code provides an appropriate method to clean itself up.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ko.bindingHandlers.myWidget = {
    init: function (element, valueAccessor) {
        var myWidget = $(element).someWidget({
            value: valueAccessor()
        });

        ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
            // or whatever code is necessary to clean-up the widget
            if (myWidget && typeof myWidget.destroy === "function") {
                myWidget.destroy();
            }
        })
    }
};

Reviewing the tools/API for clean-up in Knockout

  1. dispose function. Can be called on a manual subscription or computed to remove any subscriptions to it.

  2. ko.utils.domNodeDisposal.addDisposeCallback - adds code to run when Knockout removes an element and is normally used in a custom binding.

  3. ko.pureComputed - this new type of computed added in KO 3.2, handles removing subscriptions itself when nobody is interested in its value.

  4. disposeWhenNodeIsRemoved option to a computed - in some cases, you may find it useful to create one or more computeds in the init function of a custom binding to have better control over how you handle changes to the various observables the binding references (vs. the update function firing for changes to all observables referenced. This technique can also allow you to more easily share data between the init function and code that runs when there are changes (which normally would be in the updatefunction.

For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ko.bindingHandlers.myBinding = {
    init: function (element, valueAccessor) {
        var options = valueAccessor();

        ko.computed(function () {
            var value = ko.unwrap(options.value);

            // do something with value
        }, null, { disposeWhenNodeIsRemoved: element });


        ko.computed(function () {
            var height = ko.unwrap(options.height);

            // do something with height
        }, null, { disposeWhenNodeIsRemoved: element });
    }
};

Note that the example is passing in the disposeWhenNodeIsRemoved option to indicate that these computeds should automatically be disposed when the element is removed. This is a convenient alternative to saving a reference to these computeds and setting up a handler to call dispose by using ko.utils.domNodeDisposal.addDisposeCallback.

Keeping track of things to dispose

One pattern that I have used in applications when I know that a particular module will often be created and torn down is to do these two things:

1- When my module is being disposed, loop through all top-level properties and call dispose on anything that can be disposed. Truly it would only be necessary to dispose items that have subscribed to long-living observables (that live outside of the object itself), but easy enough to dispose of anything at the top-level when some have created “external” subscriptions.

2- Create a disposables array of subscriptions to loop over when my module is being disposed, rather than assigning every subscription to a top-level property of the module.

A snippet of a module like this might look like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var MyModule = function () {
    this.disposables = [];

    this.userStatus = ko.observable();

    this.userStatusText = ko.computed(this.getUserStatusText, this);

    this.disposables.push(myApp.currentLanguage.subscribe(this.handleLanguageChange, this));
};

ko.utils.extend(MyModule.prototype, {
    getUserStatusText: function() {
        return myApp.currentUser() + " (" + this.userStatus + ")";
    },

    handleLanguageChange: function(newLanguage) {
        // do something with newLanguage
    },

    dispose: function() {
        ko.utils.arrayForEach(this.disposables, this.disposeOne);
        ko.utils.objectForEach(this, this.disposeOne);
    },

    // little helper that handles being given a value or prop + value
    disposeOne: function(propOrValue, value) {
        var disposable = value || propOrValue;

        if (disposable && typeof disposable.dispose === "function") {
            disposable.dispose();
        }
    }
});

Conclusion

Memory leaks are not uncommon to find in long-running Knockout.js applications. Being mindful of how and when you subscribe to long-living observables from objects that are potentially short-lived can help alleviate these leaks. The APIs listed in this post will help ensure that references from subscriptions are properly removed and your applications are free of memory leaks.

Comments