P3PC is a project to review the performance of 3rd party content such as ads, widgets, and analytics. You can see all the reviews and stats on the P3PC home page. This blog post looks at Collective Media. Here are the summary stats.
impact |
Page Speed | YSlow | doc. write |
total reqs | total |
JS ungzip | DOM elems | median Δ |
---|---|---|---|---|---|---|---|---|
big | 86 | 90 | y | 6 | 7 | |||
* Stats for ads only include the ad framework and not any ad content.
** It’s not possible to gather timing stats for snippets with live ads.
|
column definitions |
I don’t have an account with Collective Media, so my friends over at Zimbio let me use their ad codes during my testing. Since these are live (paying) ads I can’t crowdsource time measurements for these ads.
Snippet Code
Let’s look at the actual snippet code:
1: | <script type=”text/javascript” > |
2: | document.write(unescape(“%3Cscript src=’http://a.collective-media.net/adj/cm.zimbio/picture;sz=300×250;ord=” + Math.round(Math.random()*10000000) + “‘ type=’text/javascript’%3E%3C/script%3E”)); |
3: | </script> |
4: | <noscript><a href=”http://a.collective-media.net/jump/cm.zimbio/picture;sz=300×250;ord=[timestamp]?” target=”_blank”><img src=”http://a.collective-media.net/ad/cm.zimbio/picture;sz=300×250;ord=[timestamp]?” width=”300″ height=”250″ border=”0″ alt=”"></a></noscript> |
Lines 1-3 use document.write to insert the a.collective-media.net/adj/cm.zimbio/picture
script. Line 4 provides a NOSCRIPT block in case JavaScript is not available.
Performance Analysis
This HTTP waterfall chart was generated by WebPagetest.org using IE 7 with a 1.5Mbps connection from Dulles, VA. In my analysis of ad snippets I focus only on the ad framework, not on the actual ads. The Collective Media ad framework is composed of 6 HTTP requests: items 2, 3, 4, 5, 11&12, and 13.
Keep in mind that collective-media-waterfall.png
represents the actual content on the main page. Notice how that image is pushed back to item 8 in the waterfall chart. In this one page load, this main content is blocked for 471 + 228 + 508 + 136 = 1343 milliseconds by the ad framework (and another 238 ms by the ad itself).
Let’s step through each request. The requests that are part of the ad framework are bolded.
- item 1: compare.php – The HTML document.
- item 2: a.collective-media.net/adj/cm.zimbio/picture – The main Collective Media script. This script is tiny – less than 400 bytes. It contains a document.write line that inserts the k.collective-media.net/cmadj/cm.zimbio/picture script (item 3).
- item 3: k.collective-media.net/cmadj/cm.zimbio/picture – This was inserted as a script (by item 2). Instead of returning JavaScript code, it redirects to ak1.abmr.net/is/k.collective-media.net (item 4).
- item 4: ak1.abmr.net/is/k.collective-media.net – This is a redirect from item 3 that itself redirects to k.collective-media.net/cmadj/cm.zimbio/picture (item 5).
- item 5: k.collective-media.net/cmadj/cm.zimbio/picture – Most of the work of the Collective Media ad framework is done in this script. It dynamically inserts other scripts that contain the actual ad.
- item 6: ad.doubleclick.net/adj/cm.zimbio/picture – A script that uses document.write to insert the actual ad.
- item 7: adc_predlend_fear_300x250.jpg – The ad image.
- item 8: collective-media-waterfall.png – The waterfall image representing the main page’s content.
- item 9: favicon.ico – My site’s favicon.
- item 10: cm.g.doubleclick.net/pixel – DoubleClick beacon.
- item 11: l.collective-media.net/log – Collective Media beacon that fails.
- item 12: l.collective-media.net/log – Retry of the Collective Media beacon.
- item 13: a.collective-media.net/idpair – Another Collective Media beacon.
Items 2-5 are part of the ad framework. They have a dramatic impact on performance because of the way they’re daisy chained together:
All of these requests are performed sequentially. This is the main reason why the main content in the page (collective-media-waterfall.png) is delayed 1343 milliseconds.
Here are some of the performance issues with this snippet.
1. The redirects cause sequential downloads.
A redirect is almost as bad as a script when it comes to blocking. The redirect from b.scorecardresearch.com/b to /b2 causes those two resources to happen sequentially. It would be better to avoid the redirect if possible.
2. The scripts block the main content of the page from loading.
It would be better to load the script without blocking, similar to what BuySellAds.com does. In this case there are two blocking scripts that are part of the ad framework (and more that are part of the actual ad).
3. The ad is inserted using document.write.
Scripts that use document.write slow down the page because they can’t be loaded asynchronously. Inserting ads into a page without using document.write can be tricky. BuySellAds.com solves this problem by creating a DIV with the desired width and height to hold the ad, and then setting the DIV’s innerHTML.
4. The beacon returns a 200 HTTP status code.
I recommend returning a 204 (No Content) status code for beacons. A 204 response has no body and browsers will never cache them, which is exactly what we want from a beacon. In this case, the image body is less than 100 bytes. Although the savings are minimal, using a 204 response for beacons is a good best practice.
There’s one other part of the Collective Media ad framework I’d like to delve into: how scripts are loaded.
The code snippet given to publishers loads the initial script using document.write. It appears this is done to inject a random number into the URL, as opposed to using Cache-Control headers:
1: | <script type=”text/javascript” > |
2: | document.write(unescape(“%3Cscript src=’http://a.collective-media.net/adj/cm.zimbio/picture;sz=300×250;ord=” + Math.round(Math.random()*10000000) + “‘ type=’text/javascript’%3E%3C/script%3E”)); |
3: | </script> |
That initial script (item 2 in the waterfall chart) returns just one line of JavaScript that does another document.write to insert a script (item 3), again inserting a random number into the URL:
1: | document.write(‘<scr’+'ipt language=”javascript” src=”http://k.collective-media.net/cmadj/cm.zimbio/picture;sz=300×250;ord=2381217;ord1=’ +Math.floor(Math.random() * 1000000) + ‘;cmpgurl=’+escape(escape(cmPageURL))+’?”>’); document.write(‘</scr’+'ipt>’); |
That script request (item 3) is a redirect which leads to another redirect (item 4), which returns a heftier script (item 5) that starts to insert the actual ad. At the end of this script is a call to CollectiveMedia.createAndAttachAd
. Here’s that function (unminified):
1: | createAndAttachAd:function(h,c,a,d,e){ |
2: | var f=document.getElementsByTagName(“script”); |
3: | var b=f[f.length-1]; |
4: | if(b==null){ return; } |
5: | var i=document.createElement(“script”); |
6: | i.language=”javascript”; |
7: | i.setAttribute(“type”,”text/javascript”); |
8: | var j=”"; |
9: | j+=”document.write(‘<scr’+'ipt language=\”javascript\” src=\”"+c+”\”></scr’+'ipt>’);”; |
10: | var g=document.createTextNode(j); |
11: | b.parentNode.insertBefore(i,b); |
12: | appendChild(i,j); |
13: | if(e){ |
14: | var k=new cmIV_(); |
15: | k._init(h,i.parentNode,a,d); |
16: | } |
17: | }, |
In lines 2-7 & 11 a script element is created and inserted into the document. It’s debatable if you need to set the language (line 6) and type (line 7), but that’s minor. Using insertBefore instead of appendChild is a new pattern I’ve just started seeing that is more robust, so it’s nice to see that here. Lines 7-8 create a string of JavaScript to insert an external script using document.write. This could be one line, but again, that’s minor.
Then things get a little strange. Line 10 creates a text node element (“g”) that’s never used. In line 11 the script element is inserted into the document. Then a home built version of appendChild is called. This function is added to global namespace (ouch). Here’s what that function looks like:
1: | function appendChild(a,b){ |
2: | if(null==a.canHaveChildren||a.canHaveChildren){ |
3: | a.appendChild(document.createTextNode(b)); |
4: | } |
5: | else{ |
6: | a.text=b; |
7: | } |
8: | } |
OK. To wrap this up: A script element is created dynamically and inserted in the document. Then a string of JavaScript is injected into this script element. That line of JavaScript document.writes an external script request into the page. If that seems convoluted to you, you’re not alone. It took me awhile to wrap my head around this.
A cleaner approach would be to set the SRC property of the dynamic script element, rather than document.writing the script into the page. This would reduce the amount of code (small win), but more importantly avoiding document.write opens the door for loading ads asynchronously. This is what’s required to reach a state where ad content and publisher content co-exist equally in web pages.