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, не вызывая утечки памяти?
РЕДАКТИРОВАТЬ
Вот скриншот отдельных узлов:
Ответы
Ответ 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 для каждого из них (это приведет к удалению всех оконных событий-слушателей из окна, Вам нужно добавить проверки, чтобы увидеть, что вы не удаляете что-то важное)
Это решило проблему утечки памяти.