Async Ads with HTML Imports

November 16, 2013 5:05 am | 15 Comments

Scripts loaded in the typical way (<script src="a.js"></script>) block rendering which is bad for performance. The solution to this problem is to load scripts asynchronously. This is the technique used by the best 3rd party snippets, for example, Google Analytics, Tweet button, Facebook SDK, and Google+ Follow button.

Ads are probably the most prevalent 3rd party content on the Web. Unfortunately, most ads can’t be loaded asynchronously because they use document.write. (Doing document.write in an async script causes the entire page to be erased. We’ll see this in the examples below.) Some teams (GhostWriter, PageSpeed) have tried to solve the async document.write problem but it requires a lot of code and edge cases exist.

In my recent investigations into the performance of HTML Imports I discovered a way to make ads load asynchronously – even ads that use document.write. Let’s take a look at how HTML imports block, how to make HTML imports asynchronous, the dangers of doing this with document.write, and the workaround to make this all work.

HTML imports block rendering

HTML imports block rendering if used in the default way. This is unfortunate, especially given that this is a recent addition to HTML when the importance of not blocking rendering is well known. The HTML Imports spec is still a working draft, so it’s possible this could be switched so that they load asynchronously by default.

Nevertheless, HTML imports are typically created like this:

<link rel="import" href="import.php">

Content from the imported HTML document is inserted like this:

<div id=import-container></div>
<script>
var link = document.querySelector('link[rel=import]');
var content = link.import.querySelector('#imported-content');
document.getElementById('import-container').appendChild(content.cloneNode(true));
</script>

The LINK tag itself doesn’t block rendering – the browser knows the import can be downloaded asynchronously. But rendering is blocked when the browser encounters the first SCRIPT tag following the LINK. This behavior is demonstrated in the sync.php test page. To make the blocking observable, the import takes five seconds to download and then the pink “IMPORTED CONTENT” is displayed. The SCRIPT block is in the middle of the page so the first paragraph IS rendered, but the last paragraph IS NOT rendered until after five seconds. This demonstrates that HTML imports block rendering.

Running the examples: Currently HTML imports only work in Chrome Canary and you have to turn on the following flags in chrome://flags/: Experimental Web Platform features, Experimental JavaScript, and HTML Imports.

Making HTML imports async

It’s not too hard to make HTML imports asynchronous thus avoiding the default blocking behavior. Instead of using the LINK tag in markup, we create it using JavaScript:

var link = document.createElement('link');
link.rel = 'import';
link.onload = function() {
    var link = document.querySelector('link[rel=import]');
    var content = link.import.querySelector('#imported-content');
    document.getElementById('import-container').appendChild(content.cloneNode(true));
};
link.href = url;
document.getElementsByTagName('head')[0].appendChild(link);

The async.php test page shows how using this asynchronous pattern doesn’t block rendering – the last paragraph is rendered immediately, then after five seconds we see the pink “IMPORTED CONTENT” from the HTML import. This shows that HTML imports can be used without blocking the page from rendering.

HTML imports with document.write – watch out!

This is kind of weird and might be hard to grok: HTML imports have their own HTML document. BUT (here’s the complex part) any JavaScript within the HTML import is executed in the context of the main page. At least that’s the way it works now in Chrome. The spec doesn’t address this issue.

This is important because some 3rd party content (especially ads) use document.write. Some people might think that a document.write inside an HTML import would write to the HTML import’s document. But that’s not the case. Instead, document refers to the main page’s document. This can produce surprising (as in “bad”) results.

As shown in the sync docwrite.php and async docwrite.php test pages, when the HTML import contains a script that uses document.write it erases the content of the main page. If you’re uncertain whether the imported content uses document.write then it’s risky to use HTML imports. Or is it?

Safely using HTML imports with document.write

Since much 3rd party content (especially ads) use document.write, there’s a motivation to make them work with HTML imports. However, as shown by the previous examples, this can have disastrous results because when the HTML import does document.write it’s actually referencing the main page’s document and erases the main page.

There’s a simple “hack” to get around this. We can’t redefine document, but we CAN redefine document.write within the HTML import:

// inside the HTML import
document.write = function(msg) {
    document.currentScript.ownerDocument.write(msg);
};

With this change, all document.write output from scripts inside the HTML import goes to the HTML import’s document. This eliminates the problem of the HTML import clearing the main page. This fix is shown by the sync docwrite-override.php and async docwrite-override.php test pages.

Async (document.write) ads with HTML imports

Let’s see this technique in action. The async-ads.php test page includes Google’s show_ads.js to load real ads. The overridden version of document.write also echoes the output to the page so you can verify what’s happening. The document.write works and the ad is shown even though it’s loaded asynchronously.

This is a major accomplishment but there are some big caveats:

  • Although we’ve overridden document.write, there might be other JavaScript in the HTML import that assumes it’s running in the main page’s context (e.g., location.href, document.title).
  • Some people believe it would be good to disable document.write inside HTML imports, in which case ads wouldn’t work.
  • We need a fallback as support for HTML imports grows. This is possible by detecting support for HTML imports and reverting to the current (blocking) technique for ads.

Perhaps the biggest caveat is whether it’s realistic to expect website owners to do this. I don’t think a majority of websites would adopt this technique, but I like having an option to make ads async for websites that are willing to do the work. Right now, motivated website owners don’t have good alternatives for loading ads without blocking their own content on their page. I know some sites that have loaded ads at the bottom of the page in a hidden div and then clone them to the top when done, but this usually results in a drop in ad revenue because the ads load later. Using HTML imports allows the ad to be loaded at the top so we can have asynchronous behavior without a loss in ad revenue.

The goal of this post is to suggest that we find a way to solve one of today’s biggest obstacles to fast web pages: ads. The spec for HTML imports is a working draft and there’s only one implementation, so both are likely to change. My hope is we can make HTML imports asynchronous by default so they don’t block rendering, and use them as technique for achieving asynchronous ads.

15 Comments

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 Comments

P3PC: Glam Media

April 13, 2010 10:45 am | 3 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 Glam Media. Here are the summary stats.

impacton page Page Speed YSlow doc.
write
total reqs totalxfer size JS ungzip DOM elems median Δload time
big 89 83 y 11 68 kB 63 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 Glam Media, so my friends over at Zimbio let me use their ad codes during my testing. Since these are live (paying) ads I have to mask the ad codes in the snippet shown here. This means it’s not possible to crowdsource time measurements for these ads.

Snippet Code

Let’s look at the actual snippet code:

1: <script type=”text/javascript” language=”javascript” src=”http://www2.glam.com/app/site/affiliate/viewChannelModule.act?mName=viewAdJs&affiliateId=123456789&adSize=300×250&zone=Marketplace”>
2: </script>
snippet code as of April 12, 2010

The Glam Media ad is kicked off from a single script: viewChannelModule.act. This script is loaded using normal SCRIPT SRC tags, which causes blocking in IE7 and earlier.

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 Glam Media ad framework alone constitutes 9 HTTP requests.

Let’s step through each request.

  • item 1: compare.php– The HTML document.
  • item 2: viewChannelModule.act– The main Glam Media script.
  • item 3: ad.doubleclick.net– The actual ad (not included in my analysis).
  • item 4: glamadapt_jsrv.act– Script loaded by viewChannelModule.act using document.write.
  • item 5: quant.jsQuantcastscript loaded by viewChannelModule.act using document.write.
  • item 6: beacon.jsScorecardResearchscript loaded by viewChannelModule.act using document.write.
  • item 7: glam_comscore.js– Script loaded by viewChannelModule.act using document.write.
  • item 8: pixel– Beacon sent by quant.js.
  • item 9: b.scorecardresearch.com/b– Beacon sent by glam_comscore.js. This returns a redirect to /b2 (item 11).
  • item 10: glam-media-waterfall.png– The image representing the main page’s content.
  • item 11: altfarm.mediaplex.com/ad/js/– The actual ad (not included in my analysis).
  • item 12: b.scorecardresearch.com/b2 – Another beacon sent as a result of the redirect from /b (item 9).

Keep in mind that glam-media-waterfall.pngrepresents the actual content on the main page. Notice how that image is pushed back to item 10 in the waterfall chart. In this one page load, this main content is blocked for 617 + 808 = 1425 milliseconds. Here are some of the performance issues with this snippet.

1. Too many HTTP requests.

9 HTTP requests for an ad framework (not counting the ad itself) is a lot. The fact that these come from a variety of different services exacerbates the problem because more DNS lookups are required. These 9 HTTP requests are served from 6 different domains.

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.

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.comsolves 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 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.

5. Some resources aren’t cacheable.

glam_comscore.js has no caching headers, and yet its Last-Modified date is Nov 19, 2009 (almost 5 months ago). quant.js is only cacheable for 1 day.


Much of the content in this snippet is served with good performance characteristics. The scripts are compressed and minified. One of the beacons returns a 204 No Content response, which is a nice performance optimization. But the sheer number of HTTP requests, use of document.write, and scripts loaded in a blocking fashion cause the page to load more slowly.

3 Comments

P3PC: ValueClick

April 12, 2010 11:44 am | 1 Comment

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 ValueClick. 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
med 89 98 y 3 2 kB 1 kB 1 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 ValueClick, so my friends over at Zimbio let me use their ad codes during my testing. Since these are live (paying) ads I have to mask the ad codes in the snippet shown here. This means it’s not possible to crowdsource time measurements for these ads.

Snippet Code

Let’s look at the actual snippet code:

1: <script language=”javascript” src=”http://media.fastclick.net/w/get.media?sid=12345&m=6&tp=8&d=j&t=s”></script>
2: <noscript><a href=”http://media.fastclick.net/w/click.here?sid=12345&m=6&c=1″ target=”_top”>
3: <img src=”http://media.fastclick.net/w/get.media?sid=12345&m=6&tp=8&d=s&c=1″ width=300 height=250 border=1></a></noscript>
snippet code as of April 7, 2010

A quick walk through the snippet code:

  • line 1 – Download the get.media script.
  • lines 2-3 – 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. It shows why this snippet has a significant impact on page load time even with just a few HTTP requests and a very small download size.

Keep in mind that valueclick-waterfall.png represents the actual content on the main page. Notice how that image is pushed back to item 6 in the waterfall chart. That’s because the get.media script (item 2) is downloaded using normal SCRIPT SRC tags. This blocks all subsequent HTTP requests in older browsers including IE 6&7. (Here we’re using IE7.)

In addition, the get.media script is served through a redirect in IE (but not in Firefox). For IE a total of three sequential HTTP requests must be completed before the ad is returned. The ad is inserted using document.write, which can further block the main content on the page. In this one page load, the main content (valueclick-waterfall.png) is blocked for 338 + 345 + 163 = 846 milliseconds.

In my analysis of ad snippets I focus only on the ad framework, not on the actual ads. The ValueClick ad framework is very light – just two redirects and one small script that does document.write. Therefore, there are only a few problem areas in which to look for performance improvements, but they’re big:

1. The redirects block the page.

In IE there are two redirects in front of the get.media script. This is two roundrips from the user’s browser to the ValueClick servers and back again. The fact that these redirects don’t occur in Firefox leads me to believe that there’s a workaround for IE. Given that over 50% of Internet traffic uses IE, removing these redirects would have a positive impact on a significant number of users.

2. The get.media script blocks the main content of the page from loading.

It would be better to load the script without blocking, similar to what BuySellAds.com does.

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.


Ad networks are an amazing piece of technology. Having so many different companies share such a variety of content across millions of web sites is a real accomplishment. Techniques like document.write and scripts that block have made this possible. But the Web has evolved since these techniques were considered acceptable.

It’s critical that ad providers adopt new web development patterns so they can hit that win-win-win of a fast user experience, publisher content that renders immediately, and ads that appear quickly to drive impressions and click throughs.

1 Comment

P3PC: Google AdSense

March 29, 2010 1:44 pm | 6 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 Google AdSense. 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 87 84 y 8 41 kB 76 kB 9 222 ms
column definitions
Click here to see how your browser performs compared to the median load time shown above.

After signing up for Google AdSense, you can setup different types of ads. I chose “AdSense for Content” (listed first). Here’s what an example ad looks like. (This is a static image. Go to the Compare page to see the snippet live.)

Snippet Code

Let’s look at the actual snippet code:

1: <script type=”text/javascript”><!–
2: google_ad_client = “pub-0478442537074871”;
3: /* 300×250, created 3/6/10 */
4: google_ad_slot = “4427977761”;
5: google_ad_width = 300;
6: google_ad_height = 250;
7: //–>
8: </script>
9: <script type=”text/javascript”
10: src=”http://pagead2.googlesyndication.com/pagead/show_ads.js”>
11: </script>
snippet code as of March 10, 2010

A quick walk through the snippet code:

  • lines 2-6 – Define global variables that are used by the show_ads.js script.
  • lines 9-11 – Load the show_ads.js script.

Performance Analysis

This HTTP waterfall chart was generated by WebPagetest.org using IE 7 with a 1.5Mbps connection from Dulles, VA. It reveals a lot of idiosyncrasies with scripts, browsers, and HTTP. Let’s step through each request.

  • item 1: compare.php – The HTML document.
  • item 2: show_ads.js – The main script. All other downloads are blocked because this is loaded using the SCRIPT SRC HTML tag.
  • items 3-5 – Other scripts that are loaded dynamically by show_ads.js. This dynamic loading is done by using document.write. Using document.write causes the scripts themselves to be downloaded in parallel in IE (as evidenced by the waterfall chart). However, subsequent resources are still blocked (item 7).
  • item 6: ads – The actual ad content from doubleclick.net. Google AdSense creates an iframe to hold the ad, so this is the HTML document contained in that iframe.
  • item 7: *-waterfall.png – The waterfall image in this page. This is the main content of the page. Notice how it’s blocked by the previous four scripts.
  • item 8-9: abg-en-100c-0000000.png – The “Ads by Google” image. This is loaded twice in IE because it’s referenced twice in the ad: as an IMG and as part of AlphaImageLoader.
  • item 10: sma8.js – A script loaded by the ad. Because the ad is in an iframe, this script won’t block any resources in the main page.

Now that we have a handle on the HTTP requests involved, let’s look at the most important performance issues along with recommended solutions.

1. The scripts block the main content of the page from loading.

It would be better to load the scripts without blocking. This isn’t possible with the current implementation because the ad is inserted using document.write. Calling document.write from scripts loaded asynchronously may lead to ads being inserted in the wrong location or potentially the entire page being blank. The ideal solution would be create a DIV with the desired width and height to hold the ad and load the scripts asynchronously, similar to what BuySellAds.com does.

2. Most of the resources are only cacheable for a day.

It’s understandable that show_ads.js is only cacheable for a day. If this script changed (bug fix, new feature), there would be no way to rev the filename (since the snippet is embedded in the publishers’ pages). A short expiration date ensures users will get the updated version sooner (within a day). However, expansion_embed.js, abg-en-100c-000000.png, and sma8.js are also only cacheable for a day. These should have a far future expiration date (a year or more). If there was a change to expansion_embed.js (for example), the new version could be pushed with a modified filename (expansion_embed.1.1.js) and the code in show_ads.js could be modified to reference this new filename.

3. abg-en-100c-000000.png is downloaded twice.

This is happening because an AlphaImageLoader filter is used to achieve alpha transparency in IE 6. The HTML looks like this:

1: <span style=”display:inline-block;height:16px;width:78;
2:     pxfilter:progid:DXImageTransform.Microsoft.AlphaImageLoader(
3:     src=’http://…/abg-en-100c-000000.png’);”>
4: <img src=http://…/abg-en-100c-000000.png
5:     alt=”Ads by Google” border=0 height=16 width=78
6:     style=filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0)>
7: </span>

Notice that abg-en-100c-000000.png is used in the src in both lines 3 and 4. I’m not sure why the IMG is being used with an opacity of 0 (preserve space?), but I bet there’s a workaround. Also, this should only be necessary in IE 6, so the AlphaImageLoader should be skipped for IE 7&8.

4. Five scripts are downloaded.

Some of the scripts could be combined to reduce the number of HTTP requests.


6 Comments

P3PC: BuySellAds.com

March 16, 2010 7:09 am | 4 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 BuySellAds.com. 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
small 81 92 n 3 7 kB 14 kB 9 28 ms
column definitions
Click here to see how your browser performs compared to the median load time shown above.

After signing up for BuySellAds.com, you can setup different types of ads. I chose an image-only 125×125 ad. Since this is a test page, I can’t get real ads. Check out Webdesigner Depot or All Things Cupcake to see some real ads. The folks at BuySellAds.com set me up with a test ad for demo purposes. Here’s what the test ad looks like. (This is a static image. Go to the Compare page to see the snippet live.)

Snippet Code

Let’s look at the actual snippet code:

1: <!– BuySellAds.com Ad Code –>
2: <script type=”text/javascript”>
3:     (function(){
4:         var bsa = document.createElement(‘script’);
5:         bsa.type = ‘text/javascript’;
6:         bsa.async = true;
7:         bsa.src = ‘//s3.buysellads.com/ac/bsa.js’;
8:         (document.getElementsByTagName(‘head’)[0] || document.getElementsByTagName(‘body’)[0]).appendChild(bsa);
9:     })();
10: </script>
11: <!– END BuySellAds.com Ad Code –>
12: <!– BuySellAds.com Zone Code –>
13: <div id=”bsap_1245700″ class=”bsarocks bsap_84a5f8f4c8e4c1bb2c57948fba2d9cc4″></div>
14: <!– END BuySellAds.com Zone Code –>
snippet code as of March 14, 2010

A quick walk through the snippet code:

  • lines 3-9 – Dynamically load the bsa.js script.
  • line 13 – Create a DIV to hold the ad.

Performance Analysis

This HTTP waterfall chart was generated by WebPagetest.org using IE 7 with a 1.5Mbps connection from Dulles, VA. Let’s step through each request.

  • item 1: compare.php – The HTML document.
  • item 2: bsa.js – The main script. This is loaded dynamically, so it doesn’t block other downloads.
  • item 3: *-waterfall.png – The waterfall image in this page. This is the main content of the page. Notice how it loads in parallel with bsa.js.
  • item 4: s_84a5f8f4c8e4c1bb2c57948fba2d9cc4.js – A JSON response containing the ad content. This resource was added dynamically by bsa.js.
  • item 6: 18446-1268342919.png – The image contained in the test ad.
  • item 7: imp.gif – A beacon.

The amazing thing about the BuySellAds.com snippet is that it loads ads asynchronously. Most web developers are familiar with the performance delays inflicted by ads with scripts that block the main content in the page, or even worse scripts that use document.write so any hope of parallelization is dashed. BuySellAds.com is the only ad snippet that I’ve seen that avoids these blocking issues. (If you know of others, please add a comment mentioning them.)

Asynchronous loading is achieved as a result of two things:

  1. dynamically loading bsa.js (as opposed to using normal SCRIPT SRC HTML tags)
  2. creating a DIV placeholder for the ad content (as opposed to using document.write)

How is the ad actually loaded into the DIV? The bsa.js script dynamically adds a script (s_84a5f8f4c8e4c1bb2c57948fba2d9cc4.js) containing the ad as a JSON response. That JSON response calls a function from bsa.js (interpret_json) that extracts the DIV’s id from the JSON object and sets its innerHTML. I like how the DIV’s id and classname are used, as opposed to doing this through JavaScript variables set in the snippet.

Loading ads asynchronously is a big advantage of BuySellAds.com. But there are still a few more performance improvements that could be made.

1. The size of the DIV changes causing the page to re-layout.

I used WebPagetest.org to create a filmstrip of images showing the page loading. Notice how the waterfall chart appears at 1.5 seconds. At 2.0 seconds the ad is loaded causing the waterfall chart to shift downward. It would better if the snippet set the DIV’s width and height to the appropriate values for the selected ad size.

2. bsa.js isn’t cached.

This is the script that publishers add to their pages. As such, it has to have a short expiration time so that the file cached by users is updated frequently. However, no expiration date causes browsers to check for updates too frequently. A 1 day or 1 week expiration date would strike a better balance between performance and update frequency.

3. The beacon returns a 200 HTTP status code.

I recommend returning a 204 (No Content) status code. 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, and the beacon’s HTTP headers prevent it from being cached. Although the savings are minimal, using a 204 response for beacons is a good best practice.

Hats off to the folks at BuySellAds.com for showing that asynchronous ads are possible. I’ll examine a few more ad snippets in the coming weeks. We’ll see how they stack up when it comes to performance.

Other posts in the P3PC series:


4 Comments

5b document.write scripts block in Firefox

February 10, 2010 5:58 pm | 9 Comments

This is the second of five quick posts about some browser quirks that have come up in the last few weeks.

Scripts loaded using document.write block other downloads in Firefox.

Unfortunately, document.write was invented. That problem was made a bzillion times worse when ads decided to use document.write to insert scripts into the content publisher’s page. It’s one line of code:

document.write('<script src="http://www.adnetwork.com/main.js"><\/script>');

Fortunately, most of today’s newer browsers load scripts in parallel including scripts added via document.write. But a few weeks ago I noticed that Firefox 3.6 had some weird blocking behavior in a page with ads, and tracked it down to a script added using document.write.

The document.write scripts test page demonstrates the problem. It has four scripts. The first and second are inserted using document.write. The third and fourth are loaded the normal way (via HTML using SCRIPT SRC). All four scripts are configured to take 4 seconds to download. In IE8, Chrome 4, Safari 4, and Opera 10.10, the total page load time is ~4 seconds. All the scripts, even the ones inserted using document.write, are loaded in parallel. In Firefox, the total page load time is 12 seconds (tested on 2.0, 3.0, and 3.6). The first document.write script loads from 1-4 seconds, the second document.write scripts loads from 5-8 seconds, and the final two normal scripts are loaded in parallel from 9-12 seconds.

The issues with document.write are getting more well known. Some 3rd party code snippets (including Google Analytics) are switching away from document.write. But most 3rd party snippets still use document.write to insert their code into the publisher’s page. Here’s one more reason to avoid document.write.

The five posts in this series are:

9 Comments

Using Iframes Sparingly

June 3, 2009 10:42 pm | 18 Comments

This post is based on a chapter from Even Faster Web Sites, the follow-up to High Performance Web Sites. Posts in this series include: chapters and contributing authors, Splitting the Initial Payload, Loading Scripts Without Blocking, Coupling Asynchronous Scripts, Positioning Inline Scripts, Sharding Dominant Domains, Flushing the Document Early, Using Iframes Sparingly, and Simplifying CSS Selectors.

Time to create 100 elements

Iframes provide an easy way to embed content from one web site into another. But they should be used cautiously. They are 1-2 orders of magnitude more expensive to create than any other type of DOM element, including scripts and styles. The time to create 100 elements of various types shows how expensive iframes are.

Pages that use iframes typically don’t have that many of them, so the DOM creation time isn’t a big concern. The bigger issues involve the onload event and the connection pool.

Iframes Block Onload

It’s important that the window’s onload event fire as soon as possible. This causes the browser’s busy indicators to stop, letting the user know that the page is done loading. When the onload event is delayed, it gives the user the perception that the page is slower.

The window’s onload event doesn’t fire until all its iframes, and all the resources in these iframes, have fully loaded. In Safari and Chrome, setting the iframe’s SRC dynamically via JavaScript avoids this blocking behavior.

One Connection Pool

Browsers open a small number of connections to any given web server. Older browsers, including Internet Explorer 6 & 7 and Firefox 2, only open two connections per hostname. This number has increased in newer browsers. Safari 3+ and Opera 9+ open four connections per hostname, while Chrome 1+, IE 8, and Firefox 3 open six connections per hostname. You can see the complete table in my Roundup on Parallel Connections.

One might hope that an iframe would have its own connection pool, but that’s not the case. In all major browsers, the connections are shared between the main page and its iframes. This means it’s possible for the resources in an iframe to use up the available connections and block resources in the main page from loading. If the contents of the iframe are as important, or more important, than the main page, this is fine. However, if the iframe’s contents are less critical to the page, as is often the case, it’s undesirable for the iframe to commandeer the open connections. One workaround is to set the iframe’s SRC dynamically after the higher priority resources are done downloading.

5 of the 10 top U.S. web sites use iframes. In most cases, they’re used for ads. This is unfortunate, but understandable given the simplicity of using iframes for inserting content from an ad service. In many situations, iframes are the logical solution. But keep in mind the performance impact they can have on your page. When possible, avoid iframes. When necessary, use them sparingly.

18 Comments

“Delayed Script Execution” in Opera

September 11, 2008 8:37 pm | 7 Comments

I’ve recently been evangelizing techniques for loading external scripts without blocking the rest of the page. A co-worker at Google pointed out a little known option in Opera: Delayed Script Execution. Opera’s support web site provides this explanation:

Primarily for low bandwidth devices, not well-tested on desktop. Ignore script tags until entire document is parsed and rendered, then execute all scripts in order and re-render.

This option is handy for developers today, and provides a guideline for how script deferral should be built in all browsers.

Loading Scripts Without Blocking and document.write

There are serveral advanced techniques for loading scripts without blocking other downloads and rendering. At recent conferences, I’ve talked about six alternatives for asynchronous script loading. (See slides 14-27 from my OSCON slide deck, for example.)

One limitation of these techniques is that you can’t use document.write, because when a script is loaded asynchronously the browser has already written the document. Hardcore JavaScript programmers avoid document.write, but it’s still used in the real world most notably, and infamously, by ads. A feature of Opera’s “Delayed Script Execution” option is that, even though scripts are deferred, document.write still works correctly. Opera remembers the script’s location in the page and inserts the document.write output appropriately.

An Example

I created a test page that demonstrates how this feature would benefit all browsers. The test page contains a script followed by eight images. The script is programmed to take 4 seconds to download and 4 seconds to execute. The images are each programmed to take 1 second to download. In most browsers, the default behavior is that the browser starts downloading and executing the script first, since it occurs first in the page. The images have to wait 8 seconds for the script to finish before they are downloaded and rendered. The text in the page is also blocked from rendering for 8 seconds, even though it’s already been downloaded before the script. This is because browsers block rendering of anything (text and resources) below a script until the script is done downloading. Let’s look at how this page behaves in popular browsers.

IE 6,7,8
The 8 second script blocks everything else in the page until it’s done downloading and executing. In IE 6,7 the overall page load time is ~12 seconds in IE 6,7 (8 seconds for the script plus 4 seconds for the images to load over two connections). In IE8 it’s ~10 seconds (8 seconds for the script plus 2 seconds for the images to load over six connections).IE has its own asynchronous script loading technique – the DEFER attribute. Clicking the “Load with Defer” button adds this attribute to the SCRIPT tag in the page. The results are disappointing – because the script contains document.write, the entire page is overwritten. The HTML 4.01 spec notes that DEFER should only be used when “the script is not going to generate any document content (e.g., no ‘document.write’ in javascript)”. And this example proves that IE’s built-in mechanism for script deferral should not be used for scripts that contain document.write.

Firefox 2,3
Loading the page in Firefox 2,3 produces results similar to IE — the script blocks the images. In Firefox 2 the overall page load time is ~12 seconds (8 seconds for the script plus 4 seconds for the images to load over two connections). In Firefox 3 it’s ~10 seconds (8 seconds for the script plus 2 seconds for the images to load over six connections).Since Firefox doesn’t support the DEFER attribute, you can click on the “Load Dynamically” button to load the script using one of the advanced non-blocking techniques (“script DOM element”). The page starts off loading great — the entire text is rendered immediately and the images start downloading and rendering quickly, all because the script is being loaded asynchronously. But when the script finally finishes and does its document.write, the entire page is overwritten, similar to what happens in IE. Scripts containing document.write can’t be loaded asynchronously in Firefox.

Safari 526
The script and eight images are downloaded in parallel. (Parallel script loading is a new feature in Safari 526.) But rendering is blocked for the entire 8 seconds while the script is downloaded and executed. Overall page load time is ~8 seconds, since the images and script are downloaded in parallel.To try loading the script asynchronously, click on the “Load Dynamically” button. The text is rendered immediately and the images start downloading and rendering quickly, all because the script is being loaded asynchronously. But when the script finishes and does its document.write, it’s not shown in the page. Scripts containing document.write won’t have their output shown in the page if loaded asynchronously in Safari.

Opera 9.5
Without enabling the “Delayed Script Execution” option, the page load experience is similar to the other browsers: The script is downloaded first, blocking the image downloads and text rendering for 8 seconds. The overall page load is a little faster, ~9 seconds, because Opera supports eight simultaneous connections, so the eight images download in 1 second.Enabling “Delayed Script Execution” provides an exciting alternate experience. The entire page’s text is rendered immediately. The eight images start downloading and rendering right away. In the background the 8-second script is downloading, so the overall page load time is ~8 seconds. The icing on the cake is that when the script is done executing its document.write appears where it should – a purple DIV in the box at the top. It doesn’t overwrite the page and it doesn’t disappear. It goes right where it was supposed to go.

What’s all the excitement?!

One immediate benefit of this Opera preference is that web developers can see the impact of delay-loading their JavaScript. A practice I’m advocating a lot lately is splitting a large JavaScript payload into two pieces, one of which can be loaded using an asynchronous script loading technique. This is often a complex task as the JavaScript payload grows in size and complexity. With this “Delayed Script Execution” feature in Opera, developers can get an idea of how their page would feel before undertaking the heavy lifting.

I’m even more excited about how this shows us what is possible for the future. To be able to have asynchronous script loading and preserve document.write output is like having your cake and eating it too. It’s difficult for users to find this feature in Opera. And it’s beyond the reach of web developers. But if Opera’s “Delayed Script Execution” behavior was the basis for implementing SCRIPT DEFER in all browsers, it would open the door for significant performance improvements by simply adding six characters (“DEFER “).

This is most significant for the serving of ads. Often ads are served by including a script that contains document.write to load other resources: images, flash, or even another script. Ads are typically placed high in the page, which means today’s pages suffer from slow loading ads because all their content gets blocked. And really, it’s not the pages that suffer, it’s the users. Our experience suffers. Everyone’s experience suffers. If browsers supported an implementation of SCRIPT DEFER that behaved similar to Opera’s “Delayed Script Execution” feature, we’d all be better off.

Food for thought for Safari, Firefox, and IE.

7 Comments

Velocity Wrap-up

June 26, 2008 10:51 am | 3 Comments

This week I co-chaired Velocity, the web performance and operations conference from O’Reilly. It was great! Jesse and I told the story about how the conference came about. When we proposed the conference we believed there was a community of performance and operations engineers that needed a forum to share and learn, and the attendance at Velocity confirmed this. Velocity sold out with over 600 attendees!

The lineup of speakers was great. There was a lot of material packed in a 2-day conference. I stayed in the Performance track, but wanted to attend every session in the Operations track, too. Many speakers shared their slides, and there are videos and photos from some of the talks.

(more…)

3 Comments