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 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.
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.
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?
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.
document.write
) ads with HTML importsLet’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:
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
).document.write
inside HTML imports, in which case ads wouldn’t work.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.