vuejs / core

đź–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
https://vuejs.org/
MIT License
47.72k stars 8.34k forks source link

A memory leak occurred when switching between components #6591

Closed Mario34 closed 2 years ago

Mario34 commented 2 years ago

Version

4.1.5

Reproduction link

github.com (has router) SFC Playground

Steps to reproduce

What is expected?

Stable memory usage

What is actually happening?

A large number of nodes are not reclaimed, and memory keeps growing

Mario34 commented 2 years ago

image

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

MMMMiller commented 2 years ago

same condition found

posva commented 2 years ago

Transfered from Vue router and added a repro without it

moushicheng commented 2 years ago

This does not seem to be < component /> problem using v-if,there will still be memory leaks, see here: SFC Playground

michaelperel commented 2 years ago

I am experiencing the same issue in multiple projects I am working on using vue 3 with the router

michaelperel commented 2 years ago

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.

yyx990803 commented 2 years ago

Update: the fix only addresses the simplified cases here and here.


This is fixed in https://github.com/vuejs/core/commit/fc5bdb36ed429d6c3c956f373206ce75467adaf3

Some notes:

  1. 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.

  2. This is not a regression and has been present in all versions of Vue 3.

yyx990803 commented 2 years ago

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.

yyx990803 commented 2 years ago

More investigation: the leak in the original repro is actually caused by the devtools. There is no leak in production.

yyx990803 commented 2 years ago

The "leak" only happens when:

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>
michaelperel commented 2 years ago

@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:

memory_bug
  1. The first snapshot is when the page loads, and the button has not been pressed (no evidence of memory leak).
  2. The second snapshot is when the button is pressed again and all the items are showing (no evidence of memory leak).
  3. 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.
  4. 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.

Akryum commented 2 years ago

@michaelperel The devtools are enabled on the SFC playground even in production mode.

Akryum commented 2 years ago

@michaelperel What your are describing is exactly what @yyx990803 said in the previous comment: image

michaelperel commented 2 years ago

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 :)

binvb commented 2 years ago

thank you , help me a lot