Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Nuxt huge memory usage / leakage and how to prevent

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}");`
      }
    },
  }
}

like image 529
Mojtaba Barari Avatar asked Mar 28 '21 13:03

Mojtaba Barari


People also ask

How can I detect memory leaks in my nuxt server?

To detect memory leaks, we use the Memory tool from Chrome devtools to record and compare heap snapshots. A heap snapshot shows the memory used in your app at a given moment. We can’t take snapshots directly when we inspect our local nuxt server on localhost:3000, because it would analyze the client side.

Why am I getting nuxt error during the development?

You might encounter the above error during the development of a nuxt.js (or simple vue.js) application. The following are the possible reasons which lead you to this scenario. 1. Your project is considerably large. This is common in most of the node projects.

Why can't I take snapshots of my nuxt server?

We can’t take snapshots directly when we inspect our local nuxt server on localhost:3000, because it would analyze the client side. We want to find memory leaks that are on the server side, so it’s a bit more tedious to test, and that’s what we got wrong in the first place.

What should not be used with Vue nuxt?

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.


1 Answers

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

like image 50
Mojtaba Barari Avatar answered Oct 19 '22 18:10

Mojtaba Barari