I'm experiencing a memory leak in VueJS 3. In my app there are constant updates from WebSocket, and app state objects are replaced by these updates. Old objects don't get garbage collected it seems.
I was able to reproduce the similar issue with this simple component:
<script setup>
import {reactive} from 'vue'
const state = reactive({data: {}})
updateData(0)
function updateData(count) {
state.data = {str: `Iteration ${count}`}
setTimeout(() => updateData(count + 1), 10)
}
</script>
<template>
<div>{{ state.data.str }}</div>
</template>
The memory usage grows and never stops...
UPDATE
There is memory leak even if the value, not the object is updated like this:
function updateData(count) {
state.data.str = `Iteration ${count}`
setTimeout(() => updateData(count + 1), 10)
}
And only if state modification is completely removed the memory gets garbage collected correctly:
function updateData(count) {
setTimeout(() => updateData(count + 1), 10)
}
As it turned out, the issue is caused by Vue Dev tools. Disabling them resolves the memory leak. More info: https://github.com/vuejs/core/issues/7408
Related
i'm using Vue2, I found that vue component memory leaks can cause its parent components to leak. Is this caused by the use of $parent and $children links between parent and child components?
Why not Vue break this link when destroying components to minimize the impact of leaks?
my demo:
for son components, which EventBus is not off, causing the son component to memory leak
<template>
<div>son</div>
</template>
<script>
import EventBus from '../EventBus'
export default {
mounted() {
EventBus.on('page1son1', this.testFun)
}
}
</script>
for parent components, which has no risk of memory leaks
<template>
<div>parent<son></son></div>
</template>
<script>
import son from './son'
export default {
}
I found in devtools that when the son components leaks,the parent components leaks,so i go to see the vue source code.As follows.
Vue.prototype.$destroy = function () {
const vm: Component = this
if (vm._isBeingDestroyed) {
return
}
callHook(vm, 'beforeDestroy')
vm._isBeingDestroyed = true
// remove self from parent
const parent = vm.$parent
if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
remove(parent.$children, vm)
}
// teardown scope. this includes both the render watcher and other
// watchers created
vm._scope.stop()
// remove reference from data ob
// frozen object may not have observer.
if (vm._data.__ob__) {
vm._data.__ob__.vmCount--
}
// call the last hook...
vm._isDestroyed = true
// invoke destroy hooks on current rendered tree
vm.__patch__(vm._vnode, null)
// fire destroyed hook
callHook(vm, 'destroyed')
// turn off all instance listeners.
vm.$off()
// remove __vue__ reference
if (vm.$el) {
vm.$el.__vue__ = null
}
// release circular reference (#6759)
if (vm.$vnode) {
vm.$vnode.parent = null
}
In the source code of Vue, I didn't find a place to break the reference of $parent, so I guess it is because the $parent of the child component refers to the parent component, so the memory leak of the child component will cause the leak of the parent component
I have a component which keeps a local partial copy of an Pinia storage data.
<template>
<h3>Order idx {{ idx }}: sum = {{ localSum }}, store sum = {{ getOrderSum() }}</h3>
<input type="number" v-model="localSum" />
<button #click="updateSum">Save</button>
</template>
<script setup>
import { useCounterStore } from '../store/counter'
import { watchEffect, ref, defineProps, watch } from 'vue'
const props = defineProps({
idx: 0
})
const store = useCounterStore()
const localSum = ref(0)
function getOrderSum() {
return store.getOrderIdxSumMap[props.idx]
}
function updateSum() {
store.setOrderSum(props.idx, localSum.value)
}
watch(
() => getOrderSum(),
(newValue) => {
console.log('i am updated')
localSum.value = newValue
}, {
immediate: true,
}
)
/*
watchEffect(() => {
console.log('i am updated')
localSum.value = getOrderSum()
})
*/
</script>
Whenever external data changes the local copy should update. Using watchEffect instead of watch causes components with modified and unsaved data to lose user input.
watchEffect behaviour description 1:
Change first order data
Click save
You'll see i am updated twice within console.
watchEffect behaviour description 2:
Change the first order data
Change the second order data
Click save on the first order
You'll see the second order changes lost
Comment out watchEffect and uncomment watch. Now everything works just fine. Is it my misconseptions or a bug worth to be reported?
Full demo
It is how watch and watchEffectsupposed to work
In short, watch and watchEffect both tracks the change of their dependencies. When the dependencies changed they act like follow:
watch recalculates its source value and then compares the oldValue with newValue. If oldValue !== newValue, it will trigger the callback.
watchEffect has no sources so it triggers the callback anyway
In your example the dependencies of both watch and watchEffect are store.getOrderIdxSumMap and props.idx. The source of watch is the function () => getOrderSum(). So when the store.getOrderIdxSumMap changed, watchEffect will always trigger the callback but watch will re-calculate the value of () => getOrderSum(). If it changed too, watch will trigger its callback
When Vue detects a dependency is changed?
Whenever you set a value for reactive data, Vue uses a function to decide if the value is changed as follows:
export const hasChanged = (value: any, oldValue: any): boolean =>
!Object.is(value, oldValue)
So setting the same value for a primitive type (number, string, boolean...) will not be considered as a value change
UPDATE:
I have achieved the desired behavior in the MCV by changing resetArray:
function resetArray() {
// myArray.value = [] // old version
myArray.value.length = 0 // new version
}
But I still don't understand why my MCV doesn't work.
ORIGINAL POST:
Background
In an app I am building, I store data in an a ref, created as const myArray = ref([]), which takes the form of an array of objects. This array is only changed in the following ways:
myArray.value[index] = {key: value}
myArray.value = [].
In particular, at no time is an object in myArray modified, it is either created or replaced.
I later added a watch which took action on every change to myArray.value. I discovered that after resetting myArray to [], the watcher stopped getting called.
Things I have tried:
I confirmed that my usage of ref follows the guidelines in this SO answer regarding ref vs reactive.
Refactoring to use watchEffect instead of watch. Did not help.
Refactoring to use reactive rather than ref. Did not help.
My Issue
In the MCV below, modifying myArray by calling addToArray works as intended: myArray.length is rendered and the first watch is triggered.
Calling resetArray triggers only the second watch, but the first watch IS NOT triggered when addToArray is called afterwards.
My Question
How can I both keep the ability to set myArray to [] and trigger actions every time myArray changes?
My MCV
View my MCV on Vue SFC Playground
The below code is the content of App.vue in a Vue project created with npm init vue#latest:
<script setup>
import {ref, watch} from "vue"
const myArray = ref([])
function addToArray() {
myArray.value.push("1")
}
function resetArray() {
myArray.value = []
}
watch(myArray.value, () => {
console.log("CLICKED!")
})
watch(myArray, () => {
console.log("RESET! clicked won't get called again!")
})
</script>
<template>
{{myArray.length}}<br />
<button #click="addToArray">CLICK ME</button><br />
<button #click="resetArray">RESET</button>
</template>
When watching a ref, use the ref itself -- not its value property -- as the watch source (the 1st argument to watch()).
To observe new array assignments or item additions/removals, pass the deep:true option (the 3rd argument to watch()):
watch(
myArray 1️⃣,
() => { /* handle change */ },
{ deep: true } 2️⃣
)
demo
I am fairly new to Vuex, and have ran into a problem I can't diagnose. My store is set up similarly to the Shopping example, and I've included the relevant module below.
The INIT action is called when the app loads, and everything functions fine. The LOOKUP action is later called from components, but freezes when calling the define mutation.
The current code is after trying several workarounds. Ultimately I'm trying to access state.pages from a component. I thought that the problem could've been because state.pages is an Object, so I made it non-reactive, and tried to make the component watch for changes in the pageCounter to retrieve the new page, but that didn't work.
I can include any other relevant information.
EDIT: Simplified the code to show more specific what the problem is.
store/modules/flashcards.js
// initial state
const state = () => ({
counter: 0,
})
// actions
const actions = {
}
// mutations
const mutations = {
increaseCounter(state) {
console.log(state.counter)
state.counter++; <----------- Code stops here
console.log(state.counter)
}
export default {
namespaced: true,
state,
getters,
actions,
mutations
}
The component that accesses the store:
<template>
<div>
<md-button #click='increaseCounter'>Test</md-button>
</div>
</template>
<script>
import { mapState, mapMutations } from 'vuex';
import FlashCardComponent from './FlashcardComponent'
export default {
computed: {
...mapState({
counter: state => state.flashcards.counter
})
},
methods: {
...mapMutations('flashcards', ['increaseCounter']),
</script>
In increaseCounter, the first console.log(state.counter) is printed, but the second one isn't. This is a very simple access pattern, so I would appreciate insight into why it's giving this error.
I figured out the issue. The strict flag was being used when creating the Store object. Everything worked once I removed that. As Tordek said, don't do this.
I suppose I need to figure out why that's a problem, as it points to the state being manipulated outside of a mutation.
<script>
import { callApi } from "./api";
export default {
name: "App",
data: function() {
return {
geoJson: {}
};
},
methods: {
fetchData() {
callApi().then(res => {
this.geoJson = res.data;
});
}
}
};
</script>
<template>
<button #click="fetchData">Fetch data</button>
</template>
each time I click a button trigger fetching data, the heap memory goes up.
I've checked there is no other variable refer to this.geoJson
not sure what cause this
thanks.
codesandbox: https://codesandbox.io/s/stoic-chaplygin-7sj56
The heap size grows because you are allocating new objects that live on the heap. Like you mention, there are no extra references to that value so when it is overwritten, the old value becomes garbage (not freed). It still exists on the heap despite not being reachable from anywhere until the garbage collector reclaims it.
Try clicking on the Collect Garbage (garbage can icon near the record button) to manually trigger it and see the heap size drop again.