Nuxt巨大的内存使用/泄漏及如何预防

24

我使用的是Nuxt v2.13和Vuetify v2,同时在默认布局中使用了keep-alive。随着我的应用程序越来越大,我越来越注意到内存问题,以至于我的应用程序需要在云服务器上至少拥有4GB RAM才能正常构建和运行。我深入研究并找到了零散的信息,因此决定分享它们并讨论解决方案。

请根据#号回答以下问题:

#1 - NuxtLink(vue-router)内存泄漏:其他人发现vue-router可能存在泄漏问题;由于与nuxt-link相关联的DOM将被预取,因此在内存使用方面也可能会有很高的使用率。所以有人建议使用html anchor替换nuxt-link,像这样:

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

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

您对这种方法有什么看法??以及Vuetify to属性,因为它们的工作方式类似于nuxt-link?

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

#2 - 动态组件加载: 由于我的应用程序是双向的,可以通过.env文件进行自定义设置,因此我不得不像这样动态和有条件地延迟加载许多组件:

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

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

这会导致高内存使用/泄漏吗?

# 3 - Nuxt事件总线:除了在我的组件中正常使用 this.$emit() 外,有时我必须使用 $nuxt.$emit()。我在 beforeDestroy 钩子中删除它们:

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

但有人告诉我,在created钩子上的监听器将是SSR,并且在CSR的beforeDestroy钩子中不会被移除。那我该怎么办?在created中加上if(process.client){}吗?

#4-全局插件:我发现这个问题以及这个文档。按照这个问题所述,我在全局添加了我的插件/软件包。那么vue.use()是个问题吗?我应该使用inject代替吗?如何使用?

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

# 5 - Vee Validate 泄漏问题: 我在 这里读到了关于它的问题,真的会导致泄漏吗?我正在使用 Vee Validate v3 :

我的 veevalidate.js 被全局添加到 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);

我的veevalidate mixin已添加到每个页面/组件中,并使用了veevalidate。(我使用了一个mixin,因为我需要使用我的vuex状态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 : 如我之前所提到的,我的应用程序使用keep-alive自身缓存许多内容,可能不会销毁/删除插件和事件侦听器。

# 7 - setTimeout : 需要使用clearTimeout来进行数据清理吗?

# 8 - Remove Plugins/Packages :这个文档中提到,一些插件/软件包即使在组件被销毁后也不会被移除,如何找到这些插件/软件包?

这里是我的软件包和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/font-awesome@4.x/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}");`
      }
    },
  }
}


我们也在调查内存使用情况。你有没有找到关于#4的任何信息或解决方案?我理解Vue.use如果不在导出功能内部是可以的。在#3中,我们在before mounted中使用$nuxt.$on,这样它就不会影响SSR。 - Eljas
对于#4,我尽可能地删除了我的全局插件,并在本地使用它们(这是我在nuxt上的第一个项目,我正在学习,所以我全局使用了所有东西!)对于#3,我在“ 挂载”钩子上使用事件侦听器,并在“ beforeDestroy”上将其删除,但有些情况下我必须在“ created”钩子上进行监听,所以我让它们存在。 - Mojtaba Barari
@Eljas 噢,还有关于#4的问题,我有一个误解; 在我提到的nuxt $inject文档中,它说在注入函数内插入Vue(例如Vue.use())可能会导致泄漏,而不是Vue.use()本身。 - Mojtaba Barari
2个回答

4
我认为现在是时候分享我的理解了(虽然很少):
#1 由于vue-router使用预取,因此取决于链接数量可能会导致大量的内存使用。在我的情况下,链接不多,所以我让它们保持原样。在nuxt中也有禁用预取的选项,如果你的应用程序非常繁忙或者在单个页面中有数百个链接,最好禁用预取。
// locally
<nuxt-link to="/" no-prefetch>link</nuxt-link>

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

#2 我没有发现动态组件有任何问题。

#3 不是使用 $nuxt.$on,而是在 created 钩子中使用 window.addEventListener 时我遇到了这个问题(事件侦听器未被删除)。因此最好尽可能将所有侦听器移至客户端(beforeMount 或 mounted)。

#4 正如我在上面的评论中提到的,为了更轻量级的初始化,我尽可能地删除了全局插件/ CSS,并在本地使用它们。但是关于 Vue.use() 的内存泄漏问题,那是我的误解!!在 Nuxt 文档中说:

不要在此函数中使用 Vue.use()、Vue.component() 和全局插件,不要将任何内容插入到 Vue 中,这是专门用于 Nuxt 注入的。它会导致服务器端的内存泄漏。

因此,在注入函数中使用 Vue.use() 可能会导致内存泄漏,而不是 Vue.use() 本身。

至于其他问题,目前还没有答案。


0

#6 不是一个好的选择。Keep-alive 是一种可以在某些情况下使用的引擎。组件级别和路径级别的缓存也可以减少 RAM 的使用。4GB 的 RAM 被用于某些事情上,我们需要更深入的了解。

#7 在未来,是的 - 将会有更多的优化作为框架的一部分,然后是渐进式的性质。

#8 来自文档

Vue 应用程序中的内存泄漏通常不是来自 Vue 本身,而是可能发生在将其他库合并到应用程序中时。

这就是为什么很难诊断。您可以使用性能选项卡查找泄漏数据的脚本,因为这是所描述问题的一部分。第二部分是缓存(localCache、sessionCache 和 ServiceWorker),因此不可行描述一个简单的方法来删除脚本。

最重要的是:Vue 的范围是一个组件,因此这可以是逐个禁用所有内容以进行诊断的策略。


1
首先,感谢您。那么,您如何在组件和路径级别上进行缓存?您能推荐一份文档吗? - Mojtaba Barari
1
参考: 组件级别:https://vuejs.org/guide/built-ins/keep-alive.html Nuxt自动缓存:https://github.com/nuxt-community/component-cache-module 路径级别外部缓存:https://github.com/ziaadini/nuxt-perfect-cache#readme 路径级别本地和多版本缓存:https://github.com/dulnan/nuxt-multi-cache - Patryk

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接