When and why does MobX’s keepAlive cause a memory leak?

Kevin Ring
Terria
Published in
5 min readDec 3, 2019

To force computed values to stay alive one can use the keepAlive: true option, but note that this can potentially create memory leaks.

The quote above is from the MobX documentation. If you’re like I was upon reading that statement, you’re probably wondering: What exactly would be leaking? If it’s only a potential leak, in what situations do we need to worry about it? And how can it be avoided?

The post is an attempt to answer these questions. But first, a little background.

It’s best if your application has little in common with this leaking pipe. (leak by Smalllike from the Noun Project)

Why use keepAlive?

MobX computed properties are just like normal JavaScript properties, with some additional perks. One of the perks is that the computed value is automatically cached. This is great when the computation function, which MobX calls a derivation, is expensive. When none of the observables that the derivation depends on have changed, repeatedly accessing the computed property just returns the same, previously-computed value. When one of the observables does change, MobX magically knows to invalidate the cache and re-run the derivation.

People new to MobX are often surprised to learn, however, that this automatic caching only works when the property is accessed in a reactive context. If we access the property outside of an autorun, observer, or similar, then the property acts just like a normal property. No caching for us.

If our derivation is expensive, this is a problem. We might be tempted to implement our own caching, if MobX is going to be all persnickety. Just squirrel the computed value away somewhere and then return it the next time we’re asked. But wait, if our computed value is derived from other observables, and any of those change, we do need to re-run the derivation. Maybe we grab all the input observables, squirrel those away too, and then next time the property is accessed we compare to the previous observable values to decide whether to re-run the derivation? This is getting complicated, and isn’t MobX supposed to deal with this sort of thing automatically?

Ah hah, there’s the keepAlive: true option! If our computed property is created with that option set, MobX will cache the computed value even when it is accessed outside of a reactive context. And it will automatically invalidate it when observed observables change. Perfect!

But wait, “it can potentially create memory leaks?” That sounds bad.

What sort of memory leaks?

Spoiler alert: The lifetime of a computed property with keepAlive: true will be extended to the longest lifetime of any of the observables it accesses.

Let’s break that down a bit. Consider this fairly contrived example:

So we have one object, an instance of the class LongLived, that sticks around for the entire duration of a loop. And then in each iteration of the loop, we create a brand new instance of class ShortLived, evaluate its property, and then throw it away.

As currently written, each instance of ShortLived becomes eligible for garbage collection after each iteration of the loop. There’s no memory leak; all is well!

But then we change ShortLived’s bar property to use keepAlive:

With that small change, each instance of ShortLived is ineligible for garbage collection until the entire loop is complete and the instance of LongLived goes out of scope. The lifetime of the ShortLived instances has been extended to match the lifetime of LongLived.

Maybe this doesn’t sound too bad, but imagine if LongLived were some kind of application-level object that stuck around for the entire lifetime of our application. Any keepAlive computed that accesses it could never be garbage collected, for the entire time your application stays running. That’s clearly a memory leak.

But Why?

In MobX, when the derivation for computed value C accesses observable O, a reference to C is added to O’s observers list. MobX uses this reference to invalidate C’s cached value when O changes. But as long as C stays in O’s list of observers, C cannot be garbage collected because there is a live reference to it.

This is true even if there are no other references to C anywhere in the application. In this case, it might seem pointless to invalidate and recompute C, because how could anyone even see the new value if there are no references to C anywhere in the application? Unfortunately, MobX has no way to determine that this is the case. Perhaps it could be done if JavaScript had something like Java’s WeakReference. But WeakMap and WeakSet are insufficient, because neither one allows their contents to be enumerated.

But, importantly, MobX only adds C to O’s observers list in these two situations:

  1. C is evaluated in a reactive context, such as an autorun or a mobx-reactobserver, or
  2. C is marked keepAlive: true.

So, in theory, an active autorun can cause a memory leak just like keepAlive: true. In practice, this is very unlikely. For a computed value evaluated in an autorun to cause a memory leak, the computed value would have to access some long-lived observable. Then, all references to that observable (outside MobX) would have to be dropped (e.g. set to undefined), without triggering the autorun to run again. In that scenario, the autorun would indeed cause a memory leak, keeping an object from being garbage collected that is inaccessible to the application.

In the unlikely even that this ever happens, though, it could be easily fixed. Just make the references to the observable themselves observable, so that setting them to undefined triggers the autorun. Or, we can just dispose the autorun!

A computed value with keepAlive: true, on the other hand, makes it very easy to inadvertently cause a memory leak. And we can’t easily dispose a computed value.

Is it ever safe to use keepAlive?

It is safe to create a computed value with keepAlive: true if the derivation only accesses observables on the same object. In this scenario, there is no extension of lifetime.

Similarly, it is safe to access observables with the same lifetime as the computed value, or where the lifetime is similar enough that the slightly extended lifetime isn’t important to the application.

Of course, it can be difficult to ensure that these requirements remain true, especially as the code evolves over time. Certainly any computed value with keepAlive: true deserves a comment mentioning the gotchas, and it warrants extra scrutiny any time it changes.

If your computed value does need to access longer-lived observables, the only way I’m aware of to avoid a memory leak is to use an explicit dispose pattern. Computed values don’t have a built-in dispose mechanism, but it’s fairly easy to create one. It looks like this:

Now, when you’re done with an object with a keepAlive: true property, call its dispose method to ensure it removes itself from any observables that it previously accessed. Like this:

No more memory leak! Unless you forget to call dispose, of course. So, if you can, it’s better to avoid keepAlive: true entirely.

--

--