Potential memory leak in computed
Vue version
3.3.4 (all 3.* versions I tested this with were affected)
Link to minimal reproduction
Steps to reproduce
Click on "access" and then on "clear". Even if you then manually trigger garbage collection in the browser, "someObject" doesn't get removed from memory. You have to click "access" again for this to happen.
What is expected?
That after "clear" is called "someObject" gets garbage collected because there shouldn't be anything referencing it.
What is actually happening?
"someObject" stays in memory until the computed property is accessed again.
System Info
No response
Any additional comments?
This was the cause of a huge memory leak in my application that took me very long to discover.
This is not a memory leak. computed
properties are cached, so when you modify the variables that a computed
property depends on, it doesn't immediately change the computed
property's value. In fact, it continues to cache the value from the last time you accessed it until the next get
call, at which point it gets recalculated and cached again. During this recalculation, it releases the previously cached value.
like @Alfred-Skyblue said above. this is not a memory leak. as far as I know, vue-devtools will lead to memory leaks. @matej-svejda Could you test in your application with vue-devtools disabled?
@Alfred-Skyblue I see, I guess that makes sense from a technical perspective. I'm not sure it's great from an API usage point of view, but I also don't see how to easily improve that.
@edison1105 I had vue-devtools turned off.
I modified your minimal reproduction to highlight the more obvious problem: computed cannot be recycled when it is no longer referenced I think this might be what's really causing your memory leak, but I'm not sure. This is obviously a bug anyway.
@johnsoncodehk If we want to solve this problem, we must run the effect
every time set
is executed, even if get
is not triggered. Perhaps we should add a parameter to it to indicate whether it needs lazy loading, like lazy: false
.
@Alfred-Skyblue I don't think the author's original problem needs fixing, it's expected behavior, but my playground link shows a different problem, someObject still doesn't get recycled even though computed itself is set to undefined.
I dug a bit more and it seems the case @johnsoncodehk discovered is an edge case.
It's true that simply setting the computed to
undefined
doesn't cause its cached value to be garbage collected.However, if we put the same test case inside a child component and unmount that child component, the computed value is cleanly garbage collected. See this case: https://gist.github.com/yyx990803/5ed0bdcb3ff74dee34de1326acf143e7
- I wasn't able to cleanly reproduce this in the SFC playground, as there might be other variables at play. A simple HTML file also allows us to more easily capture and analyze memory snapshots.
- click
access
first, then clicktoggle
, then force GC, you will see the object is collected. You can also use Memory snapshots to check for the presence ofHeavyObject
. - Note this is using the current released
3.3.7
. I've verified it's the same behavior using this PR, but withWeakRef
andFinalizationRegistry
removed.
I'd argue the 2nd case is far more common (and probably 99.9% of the case) in real world apps. I've never seen anyone manually setting computed to undefined
- they are always declared and used as a constant inside components, and expected to be garbage collected when the component is unmounted. If that works as expected, then I don't think we need to resort to FinalizationRegistry
for the unlikely usage of computed
.