I tried to use google's DFP in both Vue.js
and Angular
SPA but it seems to be causing a memory leak.
In Angular here you can see the proof of concept https://github.com/jbojcic1/angular-dfp-memory-leak-proof-of-concept. For ads, I am using ngx-dfp npm
package (https://github.com/atwwei/ngx-dfp). To reproduce pull and run the proof of concept project, go to the home page, which will have 3 ads in the feed initially, and do the heap snapshot. After that go to the page without ads by using a link in the header, do heap snapshot again and you will see that slot references are kept after the slot is destroyed which is causing the memory leak.
In Vue I have a component which creates and destroys ad slot and I am adding those dynamically in the content feed. When I leave the page, the component is destroyed and in beforeDestroy hook, I call destroySlots but it seems that some references are still there.
Here is my dfp-ad component:
<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>
I am injecting GPT and setting global config in a plugin:
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)
}
Here is one of the ads components:
<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>
Did anybody have a similar problem? Am I doing something wrong? Or maybe you just can't destroy and create slots in SPA without causing memory leak?
Here is the screenshot of the detached nodes:
We had a huge memory leak in our our vue web application We found out we had event listeners on the window, seems like dfp created them.
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);
};
We used this to save all the events listener being attached to window when the app first loaded, then when we want to remove dfp ads we iterate the array and execute window.removeEventListener on each of them (This will remove all window event listeners from window, You need to add checks to see you are not removing something important)
This solved our memory leak problem.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With