Search code examples
javascriptvue.jsmemory-managementmemory-leaksvuex

Vue memory leak when rendered components are removed


Within a Vue application I am encountering a memory leak, the scenario in which it occurs is as follows:

  • We have a component which is rendered within a v-for which contains many child components
  • When the corresponding element is removed from the array the v-for rerenders these components and correctly removes the component that corresponds to the element removed from the array.

However the allocated memory is never freed, the application starts out with ~30-40 MB of RAM usage, which increases to 200MB RAM when the v-for is rendered (and eventually goes up to more than 1GB and crashes the browser when more elements are added or when switching). When the element is removed it stays steadily at 200MB (even when manually garbage collecting), so it seems like something it retaining my component.

I have tried locating the issue with heap snapshots but it only shows a child component as retainer. I cannot locate what is causing this component to not be garbage collected. I have tried unsubscribing all event listeners on the root with this.$root.off however this does not seem to help at all...

The code itself is condifential so I cannot just share it, however if a bit of code is necessary to understand the issue please let me know, so i can provide a replicated example.

Does anyone have any ideas how I can solve this issue or has any ideas how to locate the cause of this memory leak?

UPDATE

This is the component which renders the components in the v-for:

<template>
    <b-tabs card class="tabMenu" v-model="index">
        <b-tab v-for="(tab) in tabs" @click="doSomething" @change="doSomething">
                <TabComponent :tab="tab"></TabComponent>
        </b-tab>
    </b-tabs>
</template>

<script>
    import TabComponent from "./TabComponent";

    export default {
        components: {
            TabComponent,
        },
        created: function () {
            this.$root.$on("addTab", this.addTab);
        },
        data: function () {
            return {
                tabs: this.$store.state.tabs,
            }
        },
        beforeDestroy: function(){             
            this.$root.$off("addTab");

        },
        methods: {
            addTab(tab) {
                this.$store.commit("addTab", {tab: tab});
            },
        }
    };
</script>

And the tab component it renders:

<template>
    <div @mousedown.stop>
    <!--   Other components are loaded here but not relevant    -->
        <div>

                <div v-show="conditionA">
                    <resize-observer @notify="doSomething" v-if="conditionC"></resize-observer>

<!--          This component renders many SVG elements which can be found in the heapsnapshot as DetachedSvgElements when the parent is not present anymore          -->
                    <VisualizationComponent v-show="conditionD"
                                           :tab="tab"></VisualizationComponent>
                </div>
        </div>
    </div>
</template>

<script>
    export default {
        components: {

            },
        props: {
            tab: TabObject,
        },
        data: function () {
            return {

            }
        },
        watch: {
           // Some watchers
        },
        mounted: function () {
            this.$nextTick(function () {
                // Do some calculations
                this.$root.$emit("updateSomething");
            });
        },
        created: function(){
            this.$root.$on("listen", this.doSomething);
            // And listen to more events
        },
        beforeDestroy: function(){
            this.$root.$off("listen");
            // And unsubscribe all others
        },
        computed: {
            // Quite a lot of computed props
        },
        methods: {
            // And also many methods for data processing
        }
    }
</script>

Solution

  • I had a similar issue. The object that I passed on through a property to the next component was complex and large in my case, I do not know if this is also the case for you?

    My issue was solved by changing the way of passing my object. By changing the property to a number, in my case an ID, I was able to retrieve my object in the component where the property is used (based on the ID). As a result, I did not have to pass on the entire object repeatedly. For some reason passing large objects as data props is not working as it should and causes strange behavior...

    In your case it maybe helps when you don't pass the 'tab' property directly to your component, but rather an index of the location of this element in the store and then fetching it directly from the store within your component.

    So you need to change your v-for to:

    <b-tab v-for="(tab, index) in tabs" @click="doSomething" @change="doSomething">
        <keep-alive>
            <TabComponent :tabIndex="index"></TabComponent>
        </keep-alive>
    </b-tab>
    

    And in your TabComponent:

    props: {
        tabIndex: Number,
    },
    data: function () {
        return {
            tab: this.$store.state.tabs[this.tabIndex]
        }
    }
    

    Of course this principle would also need to be applied to any child components doing the same thing to prevent any memory leaks in child components which would obviously also impact the parents. Hopefully I could help you :)