Captain Codeman Captain Codeman

Prebid.js Header Bidding with Dfp Light (Glade)

Even faster Dfp ad loading

Contents

Introduction

If you run a website that relies on ad-revenue and you use Dfp to maximize your revenue then you might have heard of Header Bidding.

This can help improve revenue by collecting bids from multiple ad providers before passing the winners on to Dfp where Adsense and Adx have a chance to out-bid them. This increases competition in the ad-auction while also avoiding the latency from chaining ad requests in a waterfall via passbacks. There’s even a Prebid.js open-source lib to handle the process which most ad providers have adapters for so it’s not too difficult to implement.

But oh dear, the Google Publisher Tag script (GPT) used by Dfp is pretty big when you care about performance (and sadly, seems to have some problems with WebComponent polyfills right now).

Fortunately, there’s a new GPT Light script which is much more lightweight as you’d expect from the name. Here’s how you can use it.

Accelerated Mobile Pages (AMP)

DFP Light or ‘Glade’ is the ad-script being used by Accelerated Mobile Pages (AMP) so it’s clearly been designed with performance in mind.

Prebid.js has support for AMP but using it means serving Dfp creatives slightly differently. Creating the pre-bid setup for Dfp is not as easy as it should be (until you create a script to automate it) but it would be nice to be able to use a single setup for both desktop and mobile and, at the same time, use the faster Dfp Light script for non-AMP requests.

Here’s what I came up with to enable that …

GPT Light Script

The easiest part is switching the GPT script that we use. We replace the old gpt.js script reference:

<script src="https://www.googletagservices.com/tag/js/gpt.js" async></script>

with a new one to load the glade.js script instead:

<script src="https://securepubads.g.doubleclick.net/static/glade.js" async></script>

Dfp Creative

Dfp serves the same creative for all prebid.js ads which simply renders the ad already received. The new ad creative uses messages to handle communication with the main window. I’ve modified the prebid.js creative slightly to reduce it’s size:

<script>
  window.addEventListener("message", function(ev) {
    var key = ev.message ? "message" : "data";
    var data;
    try {
      data = JSON.parse(ev[key]);
    } catch (e) {
      data = {};
    }
    if (data.ad) {
      document.write(data.ad);
      document.close();
    } else if (data.adUrl) {
      document.write('<IFRAME SRC="' + data.adUrl + '" FRAMEBORDER="0" SCROLLING="no" MARGINHEIGHT="0" MARGINWIDTH="0" TOPMARGIN="0" LEFTMARGIN="0" ALLOWTRANSPARENCY="true"></IFRAME>');
      document.close();
    }
  }, false);

  window.parent.postMessage(JSON.stringify({
    message: 'Prebid creative requested: %%PATTERN:hb_adid%%',
    adId: '%%PATTERN:hb_adid%%'
  }, '*');
</script>

GTP to GPT Light Shim

The normal prebid.js setup is still built around Dfp so the approach I came up with was a shim that would provide the same Dfp API but convert the ads into the Dfp Light div equivalents.

This had the advantage of keeping the rest of the Dfp and prebid.js code the same so I could easily switch between the implementations while testing.

// polyfill Object.assign
if (typeof Object.assign != 'function') {
  Object.assign = function(target, varArgs) {
    if (target == null) throw new TypeError('Cannot convert undefined or null to object');
    var to = Object(target);
    for (var i = 1; i < arguments.length; i++) {
      var source = arguments[i];
      if (source != null) {
        for (var key in source) {
          if (Object.prototype.hasOwnProperty.call(source, key)) {
            to[key] = source[key];
          }
        }
      }
    }
    return to;
  };
}

// polyfill Object.values
if (typeof Object.values != 'function') {
  Object.values = function (obj) {
    return Object.keys(obj).map(function(key) { return obj[key]; });
  }
}

// dimensions used for responsive ad size selection
var width = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth || document.body.offsetWidth;
var height = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight || document.body.offsetHeight;

// Dfp API shim
var googletag = {
  slots: {},
  targeting: {},
  initialLoad: true,
  render: function(slot) {
    var size = slot.size;
    for(var i = 0; i < slot.sizeMapping.length; i++) {
      var sm = slot.sizeMapping[i];
      if (width >= sm.viewport[0] && height >= sm.viewport[1]) {
        size = sm.sizes[0];
        break;
      }
    }
    var div = document.getElementById(slot.elementId);
    var targeting = Object.assign({}, googletag.targeting, slot.targeting);

    div.style.width = size[0] + 'px';
    div.style.height = size[1] + 'px';
    div.setAttribute('width', 'fill');
    div.setAttribute('height', 'fill');

    div.dataset.json = JSON.stringify({targeting:targeting});
    div.dataset.requestWidth = size[0];
    div.dataset.requestHeight = size[1];
    div.dataset.adUnitPath = slot.path;
    div.dataset.glade = '';

    window.glade && glade.run && glade.run(div);
  },
  pubads: function() {
    return {
      disableInitialLoad: function() {
        googletag.initialLoad = false;
      },
      enableSingleRequest: function() {},
      setTargeting: function(key, val) {
        googletag.targeting[key] = val;
      },
      getSlots: function() {
        return Object.values(googletag.slots);
      },
      refresh: function() {
        var bids = pbjs.getHighestCpmBids();
        bids.forEach(function(bid) {
          googletag.slots[bid.adUnitCode].size = [bid.width, bid.height];
          googletag.slots[bid.adUnitCode].sizeMapping = [];
        })
        Object.values(googletag.slots).forEach(googletag.render);
      }
    }
  },
  display: function(ad) {
    if (googletag.initialLoad) {
      googletag.render(googletag.slots[ad]);
    }
  },
  defineSlot: function(path, size, elementId) {
    var slot = {
      path: path,
      size: size,
      elementId: elementId,
      targeting: {},
      sizeMapping: [],
      collapseEmptyDiv: false,
      getAdUnitPath: function() {
        return this.path;
      },
      getSlotElementId: function() {
        return this.elementId;
      },
      defineSizeMapping: function(sizeMapping) {
        this.sizeMapping = sizeMapping;
        return this;
      },
      setTargeting: function(key, val) {
        this.targeting[key] = val;
        return this;
      },
      setCollapseEmptyDiv: function(val) {
        this.collapseEmptyDiv = val;
        return this;
      },
      addService: function() {
        return this;
      }
    };
    googletag.slots[slot.elementId] = slot;
    return slot;
  },
  sizeMapping: function() {
    var sizeMappingBuilder = {
      mappings: [],
      addSize: function(viewport, sizes) {
        this.mappings.push({ viewport: viewport, sizes: sizes });
        return sizeMappingBuilder;
      },
      build: function() {
        return this.mappings;
      }
    };
    return sizeMappingBuilder;
  },
  enableServices: function() {
    if (googletag.initialLoad) {
      Object.values(googletag.slots).forEach(googletag.render);
    }
  },
  cmd: {
    push: function(fn) { fn(); }
  }
};

The results from using it have been pretty good - pages load faster and the ads work just as they did before and I can use the same Dfp order + line items for all traffic, both AMP and desktop.

Maybe prebid.js will add direct support for Dfp Light at some point which would allow the shim to be removed. The new glade.js script does seem to be where the focus is now.

Responsive Ads

One issue I have found with prebid.js is that using responsive ads sometimes results in the “winning” bid being for an ad-size that isn’t valid for the page as well as unnecessary ad requests being made. I’ve patched that with some custom code for the time being which seems to improve things (both performance and revenue) which I’ll try and cover in a future post.