Google DFP в SPA (Angular, Vue) - как уничтожить объявление, и все ссылки на него, чтобы избежать утечки памяти

Я попытался использовать Google DFP в Vue.js и Angular SPA, но, похоже, он вызывает утечку памяти.

В "Угловом" вы можете увидеть доказательство концепции https://github.com/jbojcic1/angular-dfp-memory-leak-proof-of-concept. Для объявлений я использую ngx-dfp npm (https://github.com/atwwei/ngx-dfp). Чтобы воспроизвести вытягивание и запустить доказательство концептуального проекта, перейдите на главную страницу, на которой сначала будет 3 объявления в фиде, и сделайте снимок кучи. После этого перейдите на страницу без рекламы с помощью ссылки в заголовке, сделайте снимок снимок снова, и вы увидите, что ссылки на слоты сохраняются после разрушения слота, что вызывает утечку памяти.

В Vue у меня есть компонент, который создает и уничтожает рекламный блок, и я добавляю их динамически в фиде контента. Когда я покидаю страницу, компонент уничтожается, а в beforeDestroy hook я вызываю destroySlots, но кажется, что некоторые ссылки все еще существуют.

Вот мой компонент dfp-ad:

<template>
  <div :id="id" class="ad" ref="adContainer"></div>
</template>

<script>
  export default {
    name: 'dfp-ad',
    props: {
      id: { type: String, default: null },
      adName: { type: String, default: null },
      forceSafeFrame: { type: Boolean, default: false },
      safeFrameConfig: { type: String, default: null },
      recreateOnRouteChange: { type: Boolean, default: true },
      collapseIfEmpty: { type: Boolean, default: true },
      sizes: { type: Array, default: () => [] },
      responsiveMapping: { type: Array, default: () => [] },
      targetings: { type: Array, default: () => [] },
      outOfPageSlot: { type: Boolean, default: false }
    },
    data () {
      return {
        slot: null,
        networkCode: 'something',
        topLevelAdUnit: 'something_else'
      }
    },
    computed: {
      slotName () {
        return '/${this.networkCode}/${this.topLevelAdUnit}/${this.adName}'
      }
    },
    mounted () {
      this.$defineTask(() => {
        this.defineSlot()
      })
    },
    watch: {
      '$route': function (to, from) {
        if (this.recreateOnRouteChange) {
          this.$defineTask(() => {
            // this.resetTargetings()
            // We can't just change targetings because slot name is different on different pages (not sure why though)
            // too so we need to recreate it.
            this.recreateSlot()
            this.refreshContent()
          })
        }
      }
    },
    methods: {
      getState () {
        return Object.freeze({
          sizes: this.sizes,
          responsiveMapping: this.responsiveMapping,
          targetings: this.targetings,
          slotName: this.slotName,
          forceSafeFrame: this.forceSafeFrame === true,
          safeFrameConfig: this.safeFrameConfig,
          clickUrl: this.clickUrl,
          recreateOnRouteChange: this.recreateOnRouteChange,
          collapseIfEmpty: this.collapseIfEmpty === true,
          outOfPageSlot: this.outOfPageSlot
        })
      },

      setResponsiveMapping (slot) {
        const ad = this.getState()

        const sizeMapping = googletag.sizeMapping()

        if (ad.responsiveMapping.length === 0) {
          ad.sizes.forEach(function (size) {
            sizeMapping.addSize([size[0], 0], [size])
          })
        } else {
          ad.responsiveMapping.forEach(function (mapping) {
            sizeMapping.addSize(mapping.viewportSize, mapping.adSizes)
          })
        }

        slot.defineSizeMapping(sizeMapping.build())
      },

      refreshContent () {
        googletag.pubads().refresh([this.slot])
      },

      defineSlot () {
        const ad = this.getState()
        const element = this.$refs.adContainer

        this.slot = ad.outOfPageSlot
          ? googletag.defineOutOfPageSlot(ad.slotName, this.id)
          : googletag.defineSlot(ad.slotName, ad.sizes, this.id)

        if (ad.forceSafeFrame) {
          this.slot.setForceSafeFrame(true)
        }

        if (ad.clickUrl) {
          this.slot.setClickUrl(ad.clickUrl)
        }

        if (ad.collapseIfEmpty) {
          this.slot.setCollapseEmptyDiv(true, true)
        }

        if (ad.safeFrameConfig) {
          this.slot.setSafeFrameConfig(
            /** @type {googletag.SafeFrameConfig} */
            (JSON.parse(ad.safeFrameConfig))
          )
        }

        if (!ad.outOfPageSlot) {
          this.setResponsiveMapping(this.slot)
        }

        this.setTargetings(ad.targetings)

        this.slot.addService(googletag.pubads())

        googletag.display(element.id)

        this.refreshContent()
      },

      setTargetings (targetings) {
        targetings.forEach(targeting => {
          this.slot.setTargeting(targeting.key, targeting.values)
        })
      },

      resetTargetings () {
        this.slot.clearTargeting()
        this.setTargetings(this.targetings)
      },

      recreateSlot () {
        googletag.destroySlots([this.slot])
        this.defineSlot()
      }
    },

    created () {
    },

    beforeDestroy () {
      if (this.slot) {
        googletag.destroySlots([this.slot])
      }
    }
  }
</script>

<style lang="scss" scoped>

...

</style>

Я ввожу GPT и устанавливаю глобальную конфигурацию в плагине:

const dfpConfig = {
  enableVideoAds: true,
  collapseIfEmpty: true,
  centering: false,
  location: null,
  ppid: null,
  globalTargeting: null,
  forceSafeFrame: false,
  safeFrameConfig: null,
  loadGPT: true,
  loaded: false
}

const GPT_LIBRARY_URL = '//www.googletagservices.com/tag/js/gpt.js'

const googletag = window.googletag || {}
googletag.cmd = googletag.cmd || []

var scriptInjector

exports.install = function (Vue, options) {
  initialize(options)

  Vue.prototype.$hasLoaded = function () {
    return dfpConfig.loaded
  }

  Vue.prototype.$defineTask = function (task) {
    googletag.cmd.push(task)
  }
}

function initialize (options) {
  scriptInjector = options.scriptInjector

  googletag.cmd.push(() => {
    setup()
  })

  if (dfpConfig.loadGPT) {
    scriptInjector.injectScript(GPT_LIBRARY_URL).then((script) => {
      dfpConfig.loaded = true
    })

    window['googletag'] = googletag
  }
}

function setup () {
  const pubads = googletag.pubads()

  if (dfpConfig.enableVideoAds) {
    pubads.enableVideoAds()
  }

  if (dfpConfig.collapseIfEmpty) {
    pubads.collapseEmptyDivs()
  }

  // We always refresh ourselves
  pubads.disableInitialLoad()

  pubads.setForceSafeFrame(dfpConfig.forceSafeFrame)
  pubads.setCentering(dfpConfig.centering)

  addLocation(pubads)
  addPPID(pubads)
  addTargeting(pubads)
  addSafeFrameConfig(pubads)

  pubads.enableAsyncRendering()
  // pubads.enableSingleRequest()

  googletag.enableServices()
}

function addSafeFrameConfig (pubads) {
  if (!dfpConfig.safeFrameConfig) { return }
  pubads.setSafeFrameConfig(dfpConfig.safeFrameConfig)
}

function addTargeting (pubads) {
  if (!dfpConfig.globalTargeting) { return }

  for (const key in dfpConfig.globalTargeting) {
    if (dfpConfig.globalTargeting.hasOwnProperty(key)) {
      pubads.setTargeting(key, dfpConfig.globalTargeting[key])
    }
  }
}

function addLocation (pubads) {
  if (!dfpConfig.location) { return }

  if (typeof dfpConfig.location === 'string') {
    pubads.setLocation(dfpConfig.location)
    return
  }

  if (!Array.isArray(dfpConfig.location)) {
    throw new Error('Location must be an array or string')
  }

  pubads.setLocation.apply(pubads, dfpConfig.location)
}

function addPPID (pubads) {
  if (!dfpConfig.ppid) { return }

  pubads.setPublisherProvidedId(dfpConfig.ppid)
}

Вот один из компонентов рекламы:

<template>
  <div class="feed-spacer">
    <dfp-ad class="feed-ad"
            :id="adId"
            :adName="adName"
            :sizes="sizes"
            :responsiveMapping="responsiveMapping"
            :targetings="targetings"
            :recreateOnRouteChange="false">
    </dfp-ad>
  </div>
</template>

<script>
import DfpAd from '@/dfp/component/dfp-ad.vue'

export default {
  components: {DfpAd},
  name: 'feed-ad',
  props: ['instance'],
  data () {
    return {
      responsiveMapping: [
        {viewportSize: [1280, 0], adSizes: [728, 90]},
        {viewportSize: [640, 0], adSizes: [300, 250]},
        {viewportSize: [320, 0], adSizes: [300, 250]}
      ],
      sizes: [[728, 90], [300, 250]]
    }
  },
  computed: {
    adId () {
      return 'div-id-for-mid${this.instance}-leaderboard'
    },
    adName () {
      return this.$route.meta.pageId
    },
    targetings () {
      const targetings = [
        { key: 's1', values: this.$route.meta.pageId },
        { key: 'pid', values: this.$route.meta.pageId },
        { key: 'pagetype', values: this.$route.meta.pageType },
        { key: 'channel', values: this.$route.meta.pageId },
        { key: 'test', values: this.$route.query.test },
        { key: 'pos', values: 'mid${this.instance}' }
      ]

      switch (this.$route.name) {
        case 'games':
          targetings.push('some_tag', this.$route.params.slug)
          break
        case 'show':
          targetings.push('some_other_tag', this.$route.params.slug)
          break
      }
      return targetings
    }
  }
}
</script>

<style lang="scss" scoped>

  ...

</style>

У кого-то была аналогичная проблема? Я делаю что-то неправильно? Или, может быть, вы просто не можете уничтожить и создать слоты в SPA, не вызывая утечки памяти?

РЕДАКТИРОВАТЬ

Вот скриншот отдельных узлов:

enter image description here

Ответ 1

У нас была огромная утечка памяти в нашем нашем веб-приложении vue. Мы обнаружили, что у нас были прослушиватели событий в окне, похоже, что они создавали dfp.

    window.addEventHook = window.addEventListener;
    window.addEventListener = function () {
        if (!window.listenerHook)
            window.listenerHook = [];

        window.listenerHook.push({name: arguments[0], callback: arguments[1] });
        window.addEventHook.apply(window,arguments);
    };

Мы использовали это, чтобы сохранить все прослушиватели событий, прикрепленные к окну, когда приложение загрузилось первым, а затем, когда мы хотим удалить dfp-объявления, мы перебираем массив и выполняем window.removeEventListener для каждого из них (это приведет к удалению всех оконных событий-слушателей из окна, Вам нужно добавить проверки, чтобы увидеть, что вы не удаляете что-то важное)

Это решило проблему утечки памяти.