P3PC: Collective Media

April 14, 2010 7:24 am | 8 Comments

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 on page Page Speed YSlow doc.
write
total reqs total xfer size JS ungzip DOM elems median Δ load time
big 86 90 y 6 8 kB 9 kB 7 na**
* 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>
snippet code as of April 13, 2010

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:

Item 2 is a script that document.writes a request for item 3.
⇒ Item 3 redirects to item 4
⇒ Item 4 redirects to item 5 cach
⇒ Item 5 document.writes a request for item 6.

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.

8 Responses to P3PC: Collective Media

  1. Hey Steve, I see this post for the 5th time now in my Google reader. What’s happening?

    And what is for times 6 = ?

  2. > It’s debatable if you need to set the language (line 6) and type (line 7), but that’s minor.

    “language” attribute is deprecated for more than 10 years now, so I wouldn’t say it’s debatable ;) See http://perfectionkills.com/optimizing-html/#7_script_language_javascript

    `if (b == null) { }` on line 4 can be easily replaced with `if (!b)` and `i.setAttribute(‘type’, …)` — with `i.type = …`.

    In line 9, there’s no need to break script start tag into “scr” and “ipt” (it’s end tag that matters). Similarly, language attribute serves no purpose there. And speaking of script end tag, it would be cleaner to escape “</" into "<\/" instead of breaking "</script" into "</scr" + "ipt".

  3. Steve, we are working on delaying such kind of widgets to window.onload event (using xhtml-document-write replacement + DOM nodes juggling). I’m not sure whether it’s a good idea (all resources will be still loaded, but overall load time can be greater), but this lead to better UI. If idea isn’t clear – I can describe it more, but we need your opinion about this method – how potentially good it can be?

  4. @sunnybear: I’ve talked to several people who override document.write. Many of the bigger sites back away from this because of compatibility bugs and constraints (eg, more than one 3rd party snippet is doing document.write). It might be an acceptable short term workaround for smaller sites, but we as an industry need a better solution (eg, Frag Tag).

  5. @Steve:
    1) We re-define document.write only on window.onload. On this point .write can’t be applied by any widget – this just breaks the page.
    2) Can you please create a post with drawbacks after document.write re-defining? This will be very interesting.

  6. I suppose it’s not so bad for Collective Media since even Google AdSense doesn’t fare much better on this kind of analysis.

    Good to know about BuySellAds though!

  7. Using 204 for image beacons doesn’t sound very clean because the onerror eventhandler is being invoked by the img tags when 204 is received.

    I like to attach an onload handler to my beacons to remove them after use which wouldn’t work anymore. Using an onerror handler for this purpose is ugly.

  8. There’s seems to be a paradox in the anti document.write mantra: How is this script supposed to know where its output should go?

    This script, for example, uses a common technique of finding the last script on the page and inserting before that. This, however, fails when dynamically inserted unless it truly is the last script on the page (unlikely).

    Without being able to rely on the assumptions that only document.write can provide, is there another mechanism for finding “this script element” that’s works when scripts are attached to the DOM in arbitrary order?