A memory leak occurred when switching between components
Version
4.1.5
Reproduction link
github.com (has router)
SFC Playground
Steps to reproduce
- toggle between pages multiple times
- check memory usage after GC
What is expected?
Stable memory usage
What is actually happening?
A large number of nodes are not reclaimed, and memory keeps growing
Snapshot 3 is generated after switching from '5000 items' to' Empty '. You can see that the number of separated nodes is basically the same as that on the page
Transfered from Vue router and added a repro without it
This does not seem to be < component /> problem using v-if,there will still be memory leaks, see here: SFC Playground
Here is a link to @moushicheng 's example without the EmptyPage
component. This demonstrates that there is a memory leak in extremely simple cases when using v-if
. I tried with different Vue 3 versions and the results are all the same.
If you replace v-if with v-show, there is no memory leak.
Update: the fix only addresses the simplified cases here and here.
This is fixed in https://github.com/vuejs/core/commit/fc5bdb36ed429d6c3c956f373206ce75467adaf3
Some notes:
This is not technically a "memory leak". The root cause is that hoisted vnodes are retaining access to detached DOM nodes, and the detached DOM nodes in turn retain its parent / sibling trees. There is only one copy of the trees retained at any time, so the total memory does not increase over time even in the pre-fix behavior - thus not really a "leak", but does result in unnecessary memory cost.
This is not a regression and has been present in all versions of Vue 3.
Looks like the simplified reproduction is a bit different from the original case, where there is a legit memory leak that is still present in 3.2.40.
More investigation: the leak in the original repro is actually caused by the devtools. There is no leak in production.
The "leak" only happens when:
- Using the dev build
- Has Vue Devtools enabled
- Hasn't opened the Vue devtools tab
Vue Devtools buffers devtool-specific events in a global object before the devtools tab is activated. The buffered events hold references to the component instances triggering them, so those component instances, along with their DOM, are never released from memory until the devtools tab is activated and the buffer is cleared.
Although this does not affect production cases, it could cause perf issues when toggling between heavy components during development. It could also be confusing to users when debugging memory issues.
@Akryum this behavior seems to have been in devtools for a long time, I'm not sure if the memory issue is the result of a recent change or has always been there as well. Is there anyway we can rework this in the devtools to avoid it?
Minimal code needed to reproduce (using local global build):
<script src="https://unpkg.com/vue"></script>
<div id="app"></div>
<script>
const { ref, h } = Vue
const Foo = {
template: `
<div class="page">
<span v-for="i in 10">{{ i }}</span>
</div>`
}
const app = Vue.createApp({
setup() {
const ok = ref(false)
return () => {
return [
h('button', { onClick: () => (ok.value = !ok.value) }, 'toggle'),
ok.value ? h(Foo) : null
]
}
}
})
app.mount('#app')
</script>
@yyx990803 I believe the memory leak still exists, in prod, in the original case, without vue dev tools.
On a fresh install of Chrome, in private browsing, using the SFC link, here is a screenshot of the memory:
- The first snapshot is when the page loads, and the button has not been pressed (no evidence of memory leak).
- The second snapshot is when the button is pressed again and all the items are showing (no evidence of memory leak).
- The third snapshot is when the button is pressed again, and none of the items is showing, and garbage has been collected. Notice that the memory is the same as when all of the nodes are showing, but should be the same as in step 1. This is evidence of a memory leak.
- The fourth snapshot is when the button has been pressed again, twice, so the nodes are all showing again. The memory is almost twice what it should be. This is evidence of a memory leak.
The memory will not grow beyond step 4, no matter how much you press the button. So it doesn't grow indefinitely, but it does retain memory that should be freed. This is a memory leak (not unbounded).
This is a huge problem when you have webpages with many big components - in this case, the memory retention resembles an unbounded memory leak because if each component retains its original size when it goes away, and double its size when it is shown for the second time, memory use will grow tremendously.
For instance, imagine you have an application where you can toggle between tables as a dashboard, where only 1 table is shown at a time. Each table uses 100mb. When no table is being shown, the application uses ~2mb of memory. When you show a table it uses 100mb. When you toggle the table and it goes away, it still uses 100mb when it should only use ~2mb. If you toggle the table multiple times, it uses up to 200mb at most. If you do this with multiple tables that are all different components, and imagine there are 20 tables/components, you will run out of memory and crash the tab, when at most the application should use 100mb while displaying a table.
Thank you so much for clearing that up! I figured because it said “prod” that it meant they were disabled.
Thank you all so much for your work on this issue, I really really appreciate it :)