2

I'm using a Popup style UI component in a Nuxt.js base project. This is used by many pages and routes, so I declared and initiated as global component plugin when the app starts, like below:

// nuxt.config.js
plugins: [
    { src: '~/plugins/popup/index.js', mode: 'client' },
],

// plugins/toast/index.js
import Vue from 'vue';
import PopupComponent from './Popup.vue';

const PopupConstructor = Vue.extend(PopupComponent);

export default () => {
    Vue.use({
        install: () => {
            let _popup = new PopupConstructor();

            window.popup = Vue.prototype.popup = {
                appear: _popup.appear,
                disappear: _popup.disappear
            };

            _popup.vm = _popup.$mount();
            _popup.dom = _popup.vm.$el;
            document.body.appendChild(_popup.dom);
        }
    });
};

// Popup.vue
// some edit applied for the sake of simplicity
<template>
    <div
        class="popup"
        :class="{
            '--error': error,
            '--visible': visible
        }"
        ref="popup"
    >
        <div class="content" ref="content">
            <div class="title">{{title}}</div>
            <div class="text">{{detail}}</div>
        </div>
    </div>
</template>
import gsap from 'gsap';

export default {
    data: function () {
        return {
            visible: false,
            title: '',
            detail: '',
            timer: 3000,
            timeout: null,
            animationTimeout: null,
        };
    },
    created() {
    },
    mounted() {
        this.$_appear = null;
        this.$_disappear = null;
    },
    beforeDestroy() {
        this.$_appear.kill();
        this.$_appear = null;
        this.$_disappear.kill();
        this.$_disappear = null;
    },
    appear({ title, detail }) {
        if (this.visible) {
            this.clearTimeout();
        }
        this.visible = true;
        this.$_appear.kill();
        this.$_disappear.kill();
        this.title = title;
        this.detail = detail;

        this.$_showAni = gsap.to(this.$refs.popup, 0.5, {
            css: {
                top: '100px',
                opacity: 1
            },
            onComplete: () => {
                this.$_appear = null;
            }
        });
        this.timeout = window.setTimeout(() => {
            this.disappear();
        }, this.timer);
    },
    disappear() {
        this.clearTimeout();
        this.$_disappear.kill();
        this.$_disappear = gsap.to(this.$refs.popup, 0.5, {
            css: {
                top: '100px',
                opacity: 0
            },
            onComplete: () => {
                this.$_disappear = null;
                this.visible = false;
            }
        });
    },
    clearTimeout() {
        if (this.timeout) {
            window.clearTimeout(this.timeout);
            this.timeout = null;
        }
    }
}

As you see, by this code the Popup vue component's methods(appear, disappear) will be accessible through window.popup, and the component itself will be created, mounted, attached on document.

This works just fine, but the problem is it seems this leads to memory leak. As I profile the memory allocation timeline using Chrome devtool, from some point of time memory allocated with window causes retained(dangling?; could be GC-ed but left due to reference using?) memory.

Is the usage of plugin like above okay? If not, to get the same utility while preventing memory leak, which part should be corrected?

EDIT:

I added the simple version implementation code for Popup which uses GSAP library for an animation. It uses the animation for appear and disappear sequentially.

cadenzah
  • 928
  • 3
  • 10
  • 23
  • Vue components aren't supposed to be instantiated directly. Do not use them with `new`. It's unknown what causes the problem because the question doesn't contain relevant code. Global variables are used without a good reason. – Estus Flask Oct 13 '21 at 06:41
  • If `PopupComponent` is a Vue component and you want it to be available globally you can register it like this: `Vue.use(PopupComponent)`. No need to instantiate it – transGLUKator Oct 13 '21 at 06:54
  • @EstusFlask I get your point that it's not good to initiate a component in plugin using `new`. But what if I want to register specific component's method in global scope so that I can access it wherever component or code I need? `window.popup` exists for that purpose. – cadenzah Oct 13 '21 at 08:02
  • @transGLUKator I want to not only make the component available globally, but available to access the methods the global component has as well. Is it possible to do that only using `Vue.use(PopupComponent)`? – cadenzah Oct 13 '21 at 08:04
  • @EstusFlask FYI I added the code for `Popup` component – cadenzah Oct 13 '21 at 08:22
  • See https://stackoverflow.com/questions/10525582/why-are-global-variables-considered-bad-practice . If you want to share functionality across the app, this is what custom `use` functions are for. If you need to use `appear`, etc outside components, this is what imports are for. I see no single point that would cause a leak but there are several things that could cause a problem together - `window` pollution, incorrect component instantiation, GSAP, no `disappear`. That you use permanent popup instance is a problem, this is what portals are for, there would be less reasons for leaks with them. – Estus Flask Oct 13 '21 at 08:31
  • @EstusFlask Oh, my mistake, I forgot to put `disapear` method in the code. Still, thank you for the reply. I will refer to the link and your comment. – cadenzah Oct 13 '21 at 08:35
  • @EstusFlask I'm currently using Vue2.x and there is no portals in that version, is it enough to use `portal-vue` instead? – cadenzah Oct 13 '21 at 08:39
  • Yes, third-party portal is presumed, it's ok. – Estus Flask Oct 13 '21 at 09:04
  • Dont use `Vue. use` inside plugin exported function. Refer to this: https://stackoverflow.com/questions/57113787/nuxt-memory-leak-in-browser-side-when-dynamically-loading-a-component/71745978#71745978 – Bagaskara Apr 05 '22 at 03:24

0 Answers0