Description
Vue version
3.3.4
Link to minimal reproduction
Steps to reproduce
Inside the callback of a reactivity hook (e.g. watchEffect
, computed
), read a reactive value from a key-based data structure (e.g. set.has(key)
, map.get(key)
, object[key]
).
Then allow it to rerun an arbitrarily large number of times with different keys.
What is expected?
I would expect this to not result in arbitrarily large memory usage.
One solution worth considering is that a dependency should be removed as soon as the callback runs the next time and no longer uses the dependency from its previous run. But if this can be demonstrated to necessarily result in other performance drawbacks (e.g. every key-based access becoming O(n) rather than the semantically expected O(1)), then that solution may not be viable.
Ideally, it should additionally be ensured that, even if the callback never runs again (thus never expiring the unused deps), the memory for the unused deps is eventually freed by the time the callback (or computed ref) is garbage-collected, but I understand this may not be possible in JS (at least without very complex compile-time behavior).
What is actually happening?
In this minimal reproduction, one of two things eventually occurs after some time (depending on various factors):
-
The page runs out of memory and crashes.
-
The following error occurs due to the reactive value having too many deps that are never cleaned up:
RangeError: Map maximum size exceeded
at Map.set (<anonymous>)
at track (vue.runtime.esm-browser.js:501:15)
at Proxy.has (vue.runtime.esm-browser.js:821:5)
at <anonymous>:26:13
at callWithErrorHandling (vue.runtime.esm-browser.js:1550:18)
at callWithAsyncErrorHandling (vue.runtime.esm-browser.js:1558:17)
at ReactiveEffect.getter [as fn] (vue.runtime.esm-browser.js:3092:16)
at ReactiveEffect.run (vue.runtime.esm-browser.js:428:19)
at job (vue.runtime.esm-browser.js:3134:14)
at callWithErrorHandling (vue.runtime.esm-browser.js:1550:32)
System Info
No response
Any additional comments?
As a side note, when developing in Vue I often find that I have to think about memory leaks, which I really don't enjoy since how Vue's reactivity system manages memory should be an abstracted implementation detail. I often manually test if various things result in memory leaks, and this is one which did. This is one of the (few!) things I dislike about Vue: frequently having to reason about implementation details. In this case, I have to think about whether reactive values, callbacks, and/or dependencies can be garbage-collected after certain conditions occur. I'm not sure if Vue can be blamed for this problem (whether it's a problem inherent to Vue's design or to JS), but I figured it was worth noting that I believe it is a problem that has a significant negative impact on Vue's DX in my experience.
I ran into this because we generate new IDs client-side and read them from various reactive sets and maps. Our clients can easily trigger this by copying and pasting a large selection of items in the UI, for example, generating large amounts of new IDs for the new items, which Vue internally adds as reactive dependencies. Even if they undo their paste, the dependencies for the new IDs stay in memory. It doesn't help that our application is a SPA.
And even if this happens not to be a significant problem for our particular use case, the same cannot necessarily be said in general, and this is still an issue which fixing would reduce the mental model required to use Vue's reactivity. I don't want to constantly have to think about whether memory leaks are a problem just because I'm using a reactive key-based data structure in Vue. And not to mention, I never want to have to comment why certain code is necessary to avoid these memory leaks!