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:
What this technically does is goes to
myApp.currentLanguage and adds to its list of callbacks with a bound function (
languageHandler) that references
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
2. Computeds that reference long-living observables
1 2 3
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
disposemethod of a computed, like we did with a manual subscription.
- in KO 3.2, a specialized computed called a
ko.pureComputedwas added (docs here). A pure computed can be created by using
ko.computedor by pasing the
pure: trueoption 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
disposeon 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
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 (
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 (
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
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
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
Reviewing the tools/API for clean-up in Knockout
disposefunction. Can be called on a manual subscription or computed to remove any subscriptions to it.
ko.utils.domNodeDisposal.addDisposeCallback- adds code to run when Knockout removes an element and is normally used in a custom binding.
ko.pureComputed- this new type of computed added in KO 3.2, handles removing subscriptions itself when nobody is interested in its value.
disposeWhenNodeIsRemovedoption to a computed - in some cases, you may find it useful to create one or more computeds in the
initfunction of a custom binding to have better control over how you handle changes to the various observables the binding references (vs. the
updatefunction firing for changes to all observables referenced. This technique can also allow you to more easily share data between the
initfunction and code that runs when there are changes (which normally would be in the
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
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
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
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.