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
|
|
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 |
|
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
dispose
method of a computed, like we did with a manual subscription.
1 2 |
|
- in KO 3.2, a specialized computed called a
ko.pureComputed
was added (docs here). A pure computed can be created by usingko.pureComputed
rather thanko.computed
or by pasing thepure: 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). Callingdispose
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 |
|
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 |
|
Reviewing the tools/API for clean-up in Knockout
dispose
function. 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.disposeWhenNodeIsRemoved
option to a computed - in some cases, you may find it useful to create one or more computeds in theinit
function of a custom binding to have better control over how you handle changes to the various observables the binding references (vs. theupdate
function firing for changes to all observables referenced. This technique can also allow you to more easily share data between theinit
function and code that runs when there are changes (which normally would be in theupdate
function.
For example:
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 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 |
|
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.