Debugging and Fixing Memory Leaks in Vue2 Applications
This article walks through reproducing a memory‑leak scenario in a Vue2‑based terminal application, analyzes common leak causes, demonstrates how adding dynamic keys and patching Vue's sameVnode function resolves the issue, and shows how to ship the fix with patch‑package for production deployments.
The author describes a real‑world memory‑leak problem encountered on a 1 GB industrial terminal built with Vue2, where repeated rendering of a component caused DOM nodes to remain detached after destruction.
Reproduction demo : a minimal Vue2 app (App.vue and test.vue) is provided. Clicking render creates the Test component, a timer updates total after 500 ms, and clicking destroy should remove the component. In Chrome incognito mode the DOM node count rises to 2045 and does not decrease, indicating a leak.
<template>
<div id="app">
<button @click="render = true">render</button>
<button @click="render = false">destroy</button>
<Test v-if="render"/>
</div>
</template>
<script>
import Test from './test.vue'
export default {
name: 'App',
components: { Test },
data () { return { render: false } }
}
</script> <template>
<div class="test">
<div>{{ total }}</div>
<div v-for="(item,index) in 1000" :key="`${item}-${index}`" class="item">
{{ item }}ipc-prod2.8
</div>
</div>
</template>
<script>
export default {
name: 'Test',
data () { return { total: 1000 } },
mounted () { this.timer = setTimeout(() => { this.total = 10000 }, 500) },
beforeDestroy () { clearTimeout(this.timer) }
}
</script>Analysis reveals typical Vue leak patterns: uncleared timers, global event listeners, lingering DOM references, global variables, and console hijacking. Although the timer is cleared in beforeDestroy , the DOM nodes still persist.
A colleague suggests forcing a component update by changing a key when total changes. The test.vue component is modified to include a dynamic renderKey and bind it to the root element:
<div :key="renderKey">{{ total }}</div>
...mounted () { this.timer = setTimeout(() => { this.total = 10000; this.renderKey = Date.now() }, 500) }After this change, destroying the component correctly releases the DOM nodes, confirming that updating the key forces Vue to treat the vnode as new and run the proper cleanup.
To make the fix permanent, the author patches Vue's internal sameVnode function, adding a text comparison and ensuring the key check triggers a full re‑render. The modified function looks like:
function sameVnode (a, b) {
if (a.text !== b.text) return false // text differs
return (a.key === b.key &&
a.asyncFactory === b.asyncFactory &&
((a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)) ||
(isTrue(a.isAsyncPlaceholder) && isUndef(b.asyncFactory.error))));
}Because the patched Vue source resides in node_modules , the author uses patch-package to create a reproducible patch. The steps are:
Install npm i patch-package
Modify the Vue source files locally.
Run npx patch-package vue to generate a patch file.
Add a postinstall script in package.json to apply the patch automatically after npm i .
Finally, the article suggests further optimizations, such as adding a custom attribute to selectively skip forced updates, and emphasizes the importance of systematic debugging and patience.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.