Subscribe on changes!

Memory leak: onclick handler holds onto detached DOM nodes

avatar
Oct 5th 2023

Vue version

3.3.4

Link to minimal reproduction

https://github.com/nWacky/nuxt-15239/tree/vue-only-reproduction

Steps to reproduce

In App.vue there is a toggle button and TheList component

TheList component renders a list of child components TheListItem

When clicking the toggle button, about 900kb of ram is allocated to show TheList.

When clicking the button again, TheList is unmounted. 900kb is not cleared up after forcing gc in Memory inspector in Chrome.

Clicking the toggle button again allocates another 900kb for TheList.

What is expected?

When TheList is unmounted, the ram should be cleared

I think this is a memory leak

What is actually happening?

Memory inspector in Chrome points to context somewhere in createInvoker().

Removing @click="onClickButton" from TheListItem reduces leaked memory from 988Kb to 160Kb per mount/unmount cycle.

Perhaps, onclick listener is never unmounted, and is holding onto detached DOM nodes

System Info

Compiled with node `v18.17.0`
Tested in Chrome `117.0.5938.149` in incognito mode, without vue devtools plugin

Any additional comments?

Possibly related issue: nuxt/nuxt#15239

avatar
Oct 8th 2023

Adding template: { compilerOptions: { hoistStatic: false } } to vite.config.ts helped a bit

Previously, ~960kb was allocated per mount/unmount cycle.

Memory usage screenshot without hoist static

With hoistStatic: false there are ~890kb allocated per mount/unmount cycle.

Memory usage screenshot with `hoistStatic: false`

When the code is modified to manually mount/unmount onclick listener (without using @click), the retained memory drops to 137kb per mount/unmount cycle.

Manual mounting/unmounting onclick listener code example + memory screenshot

Code example:

<!-- Part of `src/components/TheListItem.vue` -->
<script setup lang="ts">
const onClickButton = () => { alert("clicked button"); };

const theButton: Ref<HTMLButtonElement | null> = ref(null);

onMounted(() => {
  if (theButton.value) {
    theButton.value.onclick = onClickButton;
  }
});

onBeforeUnmount(() => {
  if (theButton.value) {
    theButton.value.onclick = null;
  }
});
</script>

<template>
  <div>
    <button ref="theButton">Button</button>
  </div>
</template>

Retained memory in Allocation instrumentation on timeline in Chrome:

avatar
Oct 9th 2023

This is a Chrome bug. see https://bugs.chromium.org/p/chromium/issues/detail?id=1213045 remove loading="lazy" and there will be no memory leak.

<img :src="src" :title="alt" loading="lazy" />
+<img :src="src" :title="alt"/>
avatar
Oct 9th 2023

That explains 137kb leaking when mounting/unmounting a component

My primary question, however, is mostly about memory not being freed when using @click

When manually adding onclick handler in onMounted and removing in onBeforeUnmount, 137kb is never freed. (code example in previous comment)

But when using @click (like in the example below), 890kb is never freed

<script setup lang="ts">
const onClickButton = () => { };
</script>

<template>
  <div>
    <button @click="onClickButton">Button</button>
  </div>
</template>
avatar
Oct 9th 2023

My primary question, however, is mostly about memory not being freed when using @click

Please read https://github.com/vuejs/core/issues/5363 first

I removed loading="lazy" and set hoistStatic: false. No more detached DOM nodes in memory.

avatar
Oct 9th 2023

I checked it again, it seems to be caused by loading="lazy" on <img />

I guess this is a duplicate of #5363 then

Thank you 🙂