Nuxt huge memory usage / leakage and how to prevent
Asked Answered
D

2

25

I'm on Nuxt v2.13 and Vuetify v2 , also using keep-alive in my default layout. As my app got bigger and bigger , I noticed the memory problem more and more so that my app needs at least around 4GB RAM on cloud server to be built and work properly. I dug around and found scattered pieces, so decided to share them and discuss the solutions.

Please answer each one according to their #numbers

#1 - NuxtLink (vue-router) memory leakage : others found that there may be a leakage in vue-router ; also because the DOM associated with the nuxt-link will be prefetched, there also may be a high usage in memory. So someone suggested to use html anchor instead of nuxt-link like this:

<template>
  <a href="/mypage" @click.prevent="goTo('mypage')">my page link</a>
</template>

<script>
export default{
  methods:{
    goTo(link){
      this.$router.push(link)
    }
  }
}
</script>

what do you think about this approach ?? and what about Vuetify to props as they work like nuxt-link?

<template>
  <v-card to="/mypage" ></v-card>
</template>

#2 - Dynamic component load : As my app is bidirectional and customizable by .env file , i had to lazy load many of my components dynamically and conditionally like this:

<template>
  <component :is="mycomp" />
</template>

<script>
export default{
  computed:{
    mycomp(){
      return import()=>(`@/components/${process.env.SITE_DIR}/mycomp.vue`)
    }
  }
}
</script>

will this cause high memory usage/leakage ??

# 3 - Nuxt Event Bus : beside normal this.$emit() in my components, sometimes I had to use $nuxt.$emit() . i remove them all in beforeDestroy hook :

<script>
export default{
  created:{
    this.$nuxt.$on('myevent', ()=>{
      // do something
    }
  },
  beforeDestroy(){
    this.$nuxt.$off('myevent')
  }
}
</script>

but someone told me that listeners on created hook will be SSR and won't be removed in CSR beforeDestroy hook. so what should i do? add if(process.client){} to created ??

# 4 - Global Plugins : I found this issue and also this doc . i added my plugins/packages globally as mentioned in this question . So is the vue.use() a problem ? should i use inject instead? how?

// vue-product-zoomer package
import Vue from 'vue'
import ProductZoomer from 'vue-product-zoomer'
Vue.use(ProductZoomer)

# 5 - Vee Validate leakage : I read here about it , is this really cause leakage? I'm using Vee Validate v3 :

my veevalidate.js that added globally to nuxt.config.js

import Vue from 'vue'
import {  ValidationObserver, ValidationProvider, setInteractionMode } from 'vee-validate'
import { localize } from 'vee-validate';
import en from 'vee-validate/dist/locale/en.json';
import fa from 'vee-validate/dist/locale/fa.json';

localize({
    en,
    fa
});

setInteractionMode('eager')

let LOCALE = "fa";
Object.defineProperty(Vue.prototype, "locale", {
    configurable: true,
    get() {
        return LOCALE;
    },
    set(val) {
        LOCALE = val;
        localize(val);
    }
});

Vue.component('ValidationProvider', ValidationProvider);
Vue.component("ValidationObserver", ValidationObserver);

my veevalidate mixin that added to each page/component had use veevalidate . ( I used a mixin because I needed to use my vuex state lang )


import { required, email , alpha , alpha_spaces , numeric , confirmed , password } from 'vee-validate/dist/rules'
import { extend } from 'vee-validate'

export default {
    mounted() {
        extend("required", {
            ...required,
            message: `{_field_} ${this.lang.error_required}`
        });
        extend("email", {
            ...email,
            message: `{_field_} ${this.lang.error_email}`
        });
        extend("alpha", {
            ...alpha,
            message: `{_field_} ${this.lang.error_alpha}`
        });
        extend("alpha_spaces", {
            ...alpha_spaces,
            message: `{_field_} ${this.lang.error_alpha_spaces}`
        });
        extend("numeric", {
            ...numeric,
            message: `{_field_} ${this.lang.error_numeric}`
        });
        extend("confirmed", {
            ...confirmed,
            message: `{_field_} ${this.lang.error_confirmed}`
        });
        extend("decimal", {
            validate: (value, { decimals = '*', separator = '.' } = {}) => {
                if (value === null || value === undefined || value === '') {
                    return {
                        valid: false
                    };
                }
                if (Number(decimals) === 0) {
                    return {
                        valid: /^-?\d*$/.test(value),
                    };
                }
                const regexPart = decimals === '*' ? '+' : `{1,${decimals}}`;
                const regex = new RegExp(`^[-+]?\\d*(\\${separator}\\d${regexPart})?([eE]{1}[-]?\\d+)?$`);
        
                return {
                    valid: regex.test(value),
                };
            },
            message: `{_field_} ${this.lang.error_decimal}`
        })
    }
}

# 6 - Keep-Alive : As I mentioned before I'm using keep-alive in my app and that it self cache many things and may not destroy/remove plugins and event listeners.

# 7 - setTimeout : is there any need to use clearTimeout to do data clearing ??

# 8 - Remove Plugins/Packages : in this Doc it is mentioned that some plugins/packages won't be removed even after component being destroyed , how can I find those ??

here are my packages and nuxt.config

// package.json
{
  "name": "nuxt",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "dev": "nuxt",
    "build": "nuxt build",
    "start": "nuxt start",
    "generate": "nuxt generate"
  },
  "dependencies": {
    "@nuxt/http": "^0.6.0",
    "@nuxtjs/auth": "^4.9.1",
    "@nuxtjs/axios": "^5.11.0",
    "@nuxtjs/device": "^1.2.7",
    "@nuxtjs/google-gtag": "^1.0.4",
    "@nuxtjs/gtm": "^2.4.0",
    "chart.js": "^2.9.3",
    "cookie-universal-nuxt": "^2.1.4",
    "jquery": "^3.5.1",
    "less-loader": "^6.1.2",
    "nuxt": "^2.13.0",
    "nuxt-user-agent": "^1.2.2",
    "v-viewer": "^1.5.1",
    "vee-validate": "^3.3.7",
    "vue-chartjs": "^3.5.0",
    "vue-cropperjs": "^4.1.0",
    "vue-easy-dnd": "^1.10.2",
    "vue-glide-js": "^1.3.14",
    "vue-persian-datetime-picker": "^2.2.0",
    "vue-product-zoomer": "^3.0.1",
    "vue-slick-carousel": "^1.0.6",
    "vue-sweetalert2": "^3.0.5",
    "vue2-editor": "^2.10.2",
    "vuedraggable": "^2.24.0",
    "vuetify": "^2.3.9"
  },
  "devDependencies": {
    "@fortawesome/fontawesome-free": "^5.15.1",
    "@mdi/font": "^5.9.55",
    "@nuxtjs/dotenv": "^1.4.1",
    "css-loader": "^3.6.0",
    "flipclock": "^0.10.8",
    "font-awesome": "^4.7.0",
    "node-sass": "^4.14.1",
    "noty": "^3.2.0-beta",
    "nuxt-gsap-module": "^1.2.1",
    "sass-loader": "^8.0.2"
  }
}
//nuxt.config.js
const env = require('dotenv').config()
const webpack = require('webpack')

export default {
  mode: 'universal',

  loading: {
    color: 'green',
    failedColor: 'red',
    height: '3px'
  },
  router: {
    // base: process.env.NUXT_BASE_URL || '/' 
  },
  head: {
    title: process.env.SITE_TITLE + ' | ' + process.env.SITE_SHORT_DESC || '',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { hid: 'keywords', name: 'keywords', content: process.env.SITE_KEYWORDS || '' },
      { hid: 'description', name: 'description', content: process.env.SITE_DESCRIPTION || '' },
      { hid: 'robots', name: 'robots', content: process.env.SITE_ROBOTS || '' },
      { hid: 'googlebot', name: 'googlebot', content: process.env.SITE_GOOGLE_BOT || '' },
      { hid: 'bingbot', name: 'bingbot', content: process.env.SITE_BING_BOT || '' },
      { hid: 'og:locale', name: 'og:locale', content: process.env.SITE_OG_LOCALE || '' },
      { hid: 'og:type', name: 'og:type', content: process.env.SITE_OG_TYPE || '' },
      { hid: 'og:title', name: 'og:title', content: process.env.SITE_OG_TITLE || '' },
      { hid: 'og:description', name: 'og:description', content: process.env.SITE_OG_DESCRIPTION || '' },
      { hid: 'og:url', name: 'og:url', content: process.env.SITE_OG_URL || '' },
      { hid: 'og:site_name', name: 'og:site_name', content: process.env.SITE_OG_SITENAME || '' },
      { hid: 'theme-color', name: 'theme-color', content: process.env.SITE_THEME_COLOR || '' },
      { hid: 'msapplication-navbutton-color', name: 'msapplication-navbutton-color', content: process.env.SITE_MSAPP_NAVBTN_COLOR || '' },
      { hid: 'apple-mobile-web-app-status-bar-style', name: 'apple-mobile-web-app-status-bar-style', content: process.env.SITE_APPLE_WM_STATUSBAR_STYLE || '' },
      { hid: 'X-UA-Compatible', 'http-equiv': 'X-UA-Compatible', content: process.env.SITE_X_UA_Compatible || '' }
    ],
    link: [
      { rel: 'icon', type: 'image/x-icon', href: process.env.SITE_FAVICON },
      // { rel: 'shortcut icon', type: 'image/x-icon', href: process.env.SITE_FAVICON },
      { rel: 'canonical', href: process.env.SITE_REL_CANONICAL },
      // { rel: 'stylesheet', href: 'https://cdn.jsdelivr.net/npm/[email protected]/css/font-awesome.min.css' },
    ]
  },
  css: [
      '~/assets/scss/style.scss',
      '~/assets/scss/media.scss',
      '~/assets/scss/customization.scss',
      '~/assets/scss/sweetalert.scss',
      '~/assets/scss/noty.scss',
      '~/assets/scss/flipclock.scss',
      '~/assets/scss/glide.scss',
      '~/assets/scss/sorting.scss',
      '~/assets/scss/cropper.scss',
      '~/assets/scss/transitions.scss',
      '~/assets/scss/product-zoom.scss',
      'vue-slick-carousel/dist/vue-slick-carousel.css'
  ],
  plugins: [
      'plugins/mixins/reqerrors.js',
      'plugins/mixins/user.js',
      'plugins/mixins/language.js',
      'plugins/mixins/shopinfo.js',
      'plugins/mixins/formattedprice.js',
      'plugins/mixins/utils.js',
      'plugins/mixins/cms.js',
      'plugins/mixins/client.js',
      'plugins/mixins/cart.js',
      'plugins/axios.js',
      'plugins/veevalidate.js',
      'plugins/noty.js',
      'plugins/glide.js',
      '@plugins/vuetify',
      '@plugins/vuedraggable',
      '@plugins/vuedraggable',
      '@plugins/vue-slick-carousel.js',
      {src: 'plugins/vuepersiandatepicker.js', mode: 'client'},
      {src: 'plugins/cropper.js', mode: 'client'},
      {src: 'plugins/vue-product-zoomer.js', mode: 'client'},
      {src: 'plugins/vueeditor.js', mode: 'client'},
  ],
  buildModules: [
    '@nuxtjs/dotenv',
    'nuxt-gsap-module'
  ],
  modules: [
    '@nuxtjs/axios',
    '@nuxtjs/auth',
    '@nuxtjs/device',
    ['vue-sweetalert2/nuxt',
      {
        confirmButtonColor: '#29BF12',
        cancelButtonColor: '#FF3333'
      }
    ],
    'cookie-universal-nuxt',
    '@nuxtjs/gtm',
    '@nuxtjs/google-gtag',
    'nuxt-user-agent',
  ],

  gtm: {
    id: process.env.GOOGLE_TAGS_ID,
    debug: false
  },
  'google-gtag': {
    id: process.env.GOOGLE_ANALYTICS_ID,
    debug: false
  },
  gsap: {
    extraPlugins: {
      cssRule: false,
      draggable: false,
      easel: false,
      motionPath: false,
      pixi: false,
      text: false,
      scrollTo: false,
      scrollTrigger: false
    },
    extraEases: {
      expoScaleEase: false,
      roughEase: false,
      slowMo: true,
    }
  },
  axios: {
    baseURL: process.env.BASE_URL,
  },
  auth: {
      strategies: {
        local: {
          endpoints: {
            login: { url: 'auth/login', method: 'post', propertyName: 'token' },
            logout: { url: 'auth/logout', method: 'post' },
            user: { url: 'auth/info', method: 'get', propertyName: '' }
          }
        }
      },
      redirect: {
        login: '/login',
        home: '',
        logout: '/login'
      },
      cookie: {
        prefix: 'auth.',
        options: {
          path: '/',
          maxAge: process.env.AUTH_COOKIE_MAX_AGE
        }
      }
  },

  publicRuntimeConfig: {
    gtm: {
      id: process.env.GOOGLE_TAGS_ID
    },
    'google-gtag': {
      id: process.env.GOOGLE_ANALYTICS_ID,
    }
  },
  build: {
    transpile: ['vee-validate/dist/rules'],
    plugins: [
      new webpack.ProvidePlugin({
        '$': 'jquery',
        jQuery: "jquery",
        "window.jQuery": "jquery",
        '_': 'lodash'
      }),
      new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
    ],
    postcss: {
      preset: {
        features: {
          customProperties: false,
        },
      },
    },
    loaders: {
      scss: {
        prependData: `$theme_colors: ("theme_body_color":"${process.env.THEME_BODY_COLOR}","theme_main_color":"${process.env.THEME_MAIN_COLOR}","theme_main_color2":"${process.env.THEME_MAIN_COLOR2}","theme_side_color":"${process.env.THEME_SIDE_COLOR}","theme_side_color2":"${process.env.THEME_SIDE_COLOR2}","theme_link_color":"${process.env.THEME_LINK_COLOR}");`
      }
    },
  }
}

Drubbing answered 28/3, 2021 at 13:6 Comment(3)
We are investigating memory usage aswell. Did you find any information or solution for the #4? I was in the under standing that Vue.use is OK if it is not inside export functionally? On the #3 we use $nuxt.$on in before mounted so it does not affect on SSR.Studhorse
for #4 I removed my global plugins as much as I could and used them locally (this was my first project on nuxt and i was learning so I used every thing globally!) for #3 I used event listeners on mounted hook and removed them on beforeDestroy but there was some cases that I had to listen on created hook so I let them beDrubbing
@Studhorse oh also about #4 , I had a misunderstanding; in nuxt $inject docs that I mentioned , is said that plugging into Vue (e.g. Vue,use()) inside injection function may cause leakage not Vue.use() itselfDrubbing
D
4

I think it is time to share my understanding (even though it's little):

#1 as vue-router use prefetch there may be heavy memory usage depending on number of links. in my case there are not many so I let them be, there is also an option to disable prefetch in nuxt so if your app is super busy or you have hundreds of links in a single page better to disable prefetch:

// locally
<nuxt-link to="/" no-prefetch>link</nuxt-link>

// globally in nuxt.config.js
router: {
  prefetchLinks: false
}

#2 I didn't found any problem with dynamic components

#3 not with $nuxt.$on but I experienced it (event listener not being removed) when used window.addEventListener in created hook. so better moved all listeners to client side (beforeMount or mounted) as much as possible

#4 as I mentioned in a comment above I removed global plugins/css as much as I could for a lighter init and used them locally , but about Vue.use() memory leakage, that was my misunderstanding !! in nuxt doc is said that:

Don't use Vue.use(), Vue.component(), and globally, don't plug anything in Vue inside this function, dedicated to Nuxt injection. It will cause memory leak on server-side.

So using Vue.use() inside injection function may cause memory leakage not Vue.use() itself.

As for others still no answer

Drubbing answered 25/10, 2021 at 8:53 Comment(0)
U
0

#6 Is a bad choice. Keep-alive is an engine that can be used in some way. Cache on Component level and on path level can reduce RAM usage too. 4GB of RAM is used for something, we need more in-depth knowledge.

#7 In the future yes - there will be more optimization as part of the framework and then progressive nature.

#8 From documentation

Memory leaks in Vue applications do not typically come from Vue itself, rather they can happen when incorporating other libraries into an application.

This is why it is hard to diagnose. You can use the Performance tab to find scripts that leak data as this is part of the described issue. Second part is cache (localCache, sessionCache and ServiceWorker) and thus not feasible to describe a simple method to remove script.

Most important: scope of vue is a component so this can be a strategy to disable one by one all stuff to diagnose.

Unreadable answered 9/12, 2022 at 18:50 Comment(2)
First of all, Tanx. So, how do you cache on component and path level?? can u recommend a doc?Drubbing
For reference: Component level: vuejs.org/guide/built-ins/keep-alive.html Nuxt automatic one: github.com/nuxt-community/component-cache-module Path level external: github.com/ziaadini/nuxt-perfect-cache#readme Path level local and multi version: github.com/dulnan/nuxt-multi-cacheUnreadable

© 2022 - 2024 — McMap. All rights reserved.