Memory leak: onclick handler holds onto detached DOM nodes
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
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:
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"/>
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>
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.