Don’t docwrite scripts

April 10, 2012 5:29 pm | 32 Comments

In yesterday’s blog post, Making the HTTP Archive faster, one of the biggest speedups came from not using a script loader. It turns out that script loader was using document.write to load scripts dynamically. I wrote about the document.write technique in Loading Script Without Blocking back in April 2009, as well as in Even Faster Web Sites (chapter 4). It looks something like this:

document.write('<script src="' + src + '" type="text/javascript"><\/script>'):

The problem with document.write for script loading is:

  • Every DOM element below the inserted script is blocked from rendering until the script is done downloading (example).
  • It blocks other dynamic scripts (example). One exception is if multiple scripts are inserted using document.write within the same SCRIPT block (example).

Because the script loader was using document.write, the page I was optimizing rendered late and other async scripts in the page took longer to download. I removed the script loader and instead wrote my own code to load the script asynchronously following the createElement-insertBefore pattern popularized by the Google Analytics async snippet:

var sNew = document.createElement("script");
sNew.async = true;
sNew.src = "http://ajax.googleapis.com/ajax/libs/jquery/1.5.1/jquery.min.js";
var s0 = document.getElementsByTagName('script')[0];
s0.parentNode.insertBefore(sNew, s0);

Why does using document.write to dynamically insert scripts produce these bad performance effects?

It’s really not surprising if we walk through it step-by-step: We know that loading scripts using normal SCRIPT SRC= markup blocks rendering for all subsequent DOM elements. And we know that document.write is evaluated immediately before script execution releases control and the page resumes being parsed. Therefore, the document.write technique inserts a script using normal SCRIPT SRC= which blocks the rest of the page from rendering.

On the other hand, scripts inserted using the createElement-insertBefore technique do not block rendering. In fact, if document.write generated a createElement-insertBefore snippet then rendering would also not be blocked.

At the bottom of my Loading Script Without Blocking blog post is a decision tree to help developers choose which async technique to use under different scenarios. If you look closely you’ll notice that document.write is never recommended. A lot of things change on the Web, but that advice was true in 2009 and is still true today.

32 Responses to Don’t docwrite scripts

  1. Is using document.write() ever necessary? e.g. google-ads seems to use it.

  2. @Sean – advertising is a major user of document.write. Because ads *should* come last in the rendering queue and because they need to work no matter their host environment, document.write is actually a very good solution: it works everywhere, all the time. It’s truly the lowest common denominator with respect to code injection. It can and probably should be replaced by the advertising community, but I don’t see that happening any time soon.

    On the other hand, there are cases where document.write’s blocking is good. If I want to serve snippets of code (e.g. ads) but want to avoid a FOUC, I can intentionally block the DOM by placing my script calls appropriately in the document. This may not make much sense for desktop browsers on broadband but for mobile devices on 3/4G it can make the difference between a clean and sophisticated ad execution and a slow, ugly one.

    Unfortunately, style often beats substance.

  3. document.write is even more evil across various browsers.

    document.write(<script src=js_file_with_document_write('1')..);
    document.write(2);
    document.write(<script src=js_file_with_document_write('3'));

    order can be different

  4. document.write is deprecated IMHO.

  5. google map api still do this stuff.

  6. @Kyle: Let’s look at document.write for ads. One thing advertisers want is a simple “snippet” that still has a lot of power – the solution is to tell the website owner to insert a script:

    < script src="http://adnetwork.com/ads.js"> < /script>

    In the world of ads the “ads.js” script sometimes does document.write to insert an IMG. But in many other cases it uses document.write to insert a SCRIPT. Instead, it would be better if ads.js did something like 1) create a DIV or IFRAME element using createElement that is the appropriate size, and 2) use the createElement-insertBefore pattern to insert a script *asynchronously* that creates the ad content in the DIV or IFRAME created in step 1.

    It’s true that document.write works well across all platforms, but this alternative approach works in all those platforms as well (I’m not aware of any exceptions at least). Also, this alternative does NOT produce FOUC. I don’t think the website owner’s content should be blocked waiting for ads to load. Advertisers likely prefer it if the page is forced to display the ad before the main content. Therefore, while I disagree with some of your other points, I do agree that this won’t be changing any time soon. It’s going to take a revolt from web publishers demanding better performance from their ad networks.

  7. @Steve: I thoroughly agree with you that #2 is a better solution. I work with all three (doc.write, iframe, create-insert) and definitely prefer the latter, but am yet to see others make an attempt to break old habits. Large sites like CNN.com, Nascar.com and Weather.com use iframes to full effect, but I think a lot of smaller, typically less savvy sites are using whatever mechanism the ad network/provider gives them; that’s primarily doc.write.

    The small sites/apps that focus on content and site dev issues may not view advertising as anything other than a set-and-forget script that reinforces the bottom line. They probably aren’t inclined to learn the difference in these methods, incorporate them properly and reap the benefits (which will vary widely). They may or may not be amateur designers/developers. While their overall site/app performance could be increased by implementing the methods you outline, they either don’t know or don’t care about that performance gain. And it’s not the ad vendors’ responsibility to teach them. There’s a lot of legacy code that just isn’t getting deprecated, for all the usual reasons.

    I’ve found that my development approach differs depending on my target device. For apps I’m *more* okay with blocking and doc.write because the webviews are not displaying user content, just ads (my experience only, other apps are much more HTML/JS oriented and this could be a problem). For desktop browsers, I create-insert, or use a library function if I have one handy.

    I think the only thing from keeping ads from being the first load on a page, blocking everything else, is the impending user loss. Good post, thanks for raising the bar!

  8. @steve document.write in ads is evil :)
    i was developing solutions for sandbox ads with document.write
    (2 years ago for portal, month ago for mobile)

    and it was almost impossible ;)

    first idea – rewrite document.write function

    document.write = function(content) {
    save & do stuff later
    }

    we have a lots of ads on web version of portal, so it was difficult to use rewrite.
    for mobile it almost work – great for iphone, great for android 2.2 & above, but some problems with android 2.1

    our adserver also have more levels:
    ads.js:
    document.write(‘<script src="inner_ads.js"….

    inner_ads:
    document.write('<script src="more_deeper.js'

    more_deeper.js (and this is even more evil ;) document.writes are splited to lines:

    document.write('’)
    document.write(‘<script ');
    document.write(' src="blabla.js")

    second idea: create iframe (in different domain – for security)
    put evil ads.js
    and after ads fully loaded – communicate with iframe and get innerHTML and put in ad placeholder

    it works quite well for simple ads, but some ads use javascript to hide, show, animate – and it's not too easy to handle…

    maybe Shadow DOM or something like that could be solution.

    so we failed with fully working sandboxing/optimization solution for ads
    and it's sad because ads block website for 0,5-1 second (more than other things you can do to get faster website)

  9. another problem with iframes & ads

    if you put ad into iframe – you cannot show some part (with transparent parts) over other elements

    so if you have ads with mouseover effects – you cant use iframes ;/

  10. I’d love to get rid of document.write in my scripts, but so far I haven’t found a satisfying alternative.
    I write widgets based on jQuery. The first step is to check if jQuery is already loaded on the page, and if not then load it:
    window.jQuery||document.write(<script…
    Asynchronous load is not acceptable as the widget depends on jQuery.

  11. I have the same problem as Christophe. There are some other JS files that depend upon jQuery being available. I came across this old blog post https://stevesouders.com/blog/2008/12/27/coupling-async-scripts/ but not sure if it’s relevant in 2012 :)

    I also noticed on the HAR website Steve you load jQuery async in the but in the footer you have JS code that relies on jQuery. I could not figure out how you handled the dependency. Seems to me there could be a race condition sometimes.

  12. @Christophe

    I think that there are at least two solutions to this problem:

    1. You can load jquery.js and after load run “callback function” to load next JS file.
    2. You can combine them into one file. So, there will not be any problem to wait for any other files. An additional gain will be only one request to the server, instead of many.

  13. @Cezary, @Sid, @Sebastian: ControlJS is a script loader that allows you to load multiple scripts with dependencies asynchronously. There are several vendors and large publishers using it. (I’m sure they’ve hardened the code – I did minimal testing.)

    @Sid: You’re right – I do have a race condition but the page is so long and there’s so many intervening scripts I can’t trigger it. Nevertheless, I’m evaluating various solutions as part of a future blog post. ControlJS is a possible solution, but I’m trying to see if there’s something lighter weight.

    @Sebastian: Overriding document.write is complex. Checkout Gostwriter.

  14. At CloudFlare we have a very effective override for document.write in our Rocket Loader feature, but it turns out that overriding document.write is a slippery slope. Once you’ve overwritten document.write – a feature that can only be sanely used in the “pre-load” state of the page – you also have to accomodate for the fact that scripts that use document.write will behave as though they are executing in a pre-load DOM, when in fact they could be executing in a post-load DOM. Our solution involves overriding many native DOM methods, including addEventListener / attachEvent, createElement and various element lookup methods, and also many DOM properties such as the document’s readyState.

    Anyway, I mention this because some browsers that we target with Rocket Loader (Internet Explorer 8) will not allow a script to override some native DOM features UNLESS the script doing the overriding executes early enough, and ironically this has left us using document.write to place our document.write-solution on pages as early as possible.

  15. Steve, you’re suggesting using a script loader for my scenario, but this contradicts the first sentence of your post! Am I missing something?

  16. @Chris: Great insight into the tangles of overriding document.write.

    @Christophe: Good script loaders don’t use document.write.

  17. @Steve, Well, I’ll try your code. By the way, 13.8 KB is just a little bit too much weight, from my personal of view.

  18. @Steve: thanks for the clarification. I re-read your previous post and now understand better what you mean.

  19. @Cezary: I agree – 13.8K is too big. I wrote ControlJS as a proof-of-concept – until that time script loaders had drawbacks that I knew could be handled (e.g., script loader being loaded async, loading multiple scripts with dependencies async). Please do NOT reference the code off my server. Instead, take the code, incorporate it into your code base, minify it, and test it.

  20. I have to doc-write my iframe, if I try to iframe my iframe I cant get it to align top and left in the table cell.

    :(

  21. Steve,

    Your article comes in a perfect timing.

    Did you know that Google’s very own Maps API uses doc.write ?

    This doc here tells how one should load google maps async (https://developers.google.com/maps/documentation/javascript/tutorial#asynch).

    It works good by loading the FIRST script async. But check the source code and you’ll see that script, in turn, loads this: https://maps.googleapis.com/maps/api/js?sensor=false&callback=initialize

    And this script, in turn, uses doc.write in the very first few lines. What is worst: it uses it to insert a tag, which is a exactly the brickwall you hit in your ControlJS script.

    Can you write / comment about this ? Thanks !

  22. In my last post, the tag disappeared, but I meant that the second script uses doc.write to insert a new tag.

    I’m amazed that this is not some small unknown widget, this is the Google Maps API !

  23. @Felipe: When I look at https://maps.googleapis.com/maps/api/js?sensor=false&callback=initialize it does not use document.write. Perhaps you could provide a URL that uses the API and has this document.write issue?

  24. @Felipe it would not be safe for an asynchronously loaded script to use document.write (it risks reseting the document of the current page), and I also cannot see a document.write call in that script.

  25. Steve,

    I double checked to see if I wasn’t going crazy.

    My app is calling Google Maps using this URL:
    http://maps.google.com/maps/api/js?sensor=false&language=pt-BR&region=br

    I don’t know if this is an old URL, but it works, and you can see the doc.write right up there in the beginning.

    I’m going to change to the new script and see what happens. Anyways, like I said, that is live code that works to load Google Maps.

  26. @Felipe It does appear as though that script is using document.write to load additional scripts. However, I have noticed that Google’s general trend in recent history is to acknowledge that there are superior methods when it comes to page performance, and to implement them where possible. Most notably, their AdSense / DFP code now features fully asynchronous loading pathways (although I’ve noticed that the document.write pathways are still present). One can imagine that a company operating at Google’s scale cannot simply abandon customers who use legacy technologies overnight, no matter the performance gains.

  27. Guys, after some experimentation I found the distinguishing element.

    It’s the callback parameter. Go ahead and give it a try. If you pass a callback, you get the appendChild loader. If you don’t pass the callback, you get the doc.write version !

    Try these urls, only difference in them is the callback:

    http://maps.googleapis.com/maps/api/js?sensor=false&language=pt-BR&region=br

    http://maps.googleapis.com/maps/api/js?sensor=false&language=pt-BR&callback=initialize

  28. Nice explanation! Thanks.
    I think most of runtime evaluation usecases are bad.
    We better avoid them.

    I am inspired by your books.
    This is my mini framework for loading web modules with blocking page loading:
    https://github.com/ngduc/Pxax-JS-Web-Module-Loader

  29. is there any particular reason of dynamic loading of scripts? Why do not you decide about required scripts set on server side?
    I use document.write intensively for just page content generation but never scripts.

  30. Client-side dynamic loading of scripts makes sense for instance to decide which ad formats to show within a responsive design. On smaller devices you might want to show smaller ad units and/or ad units where mobile-specific ads have been trafficked.

    To that effect I’m trying to get Google DFP to work using a remix of what Steve explained above and what’s discussed in the following tread but I’m stumped, not being much of a javascript developer:
    http://stackoverflow.com/questions/8610574/inserting-and-executing-conditional-javascript

    (Yes this is a drive-by call for help!)

  31. I’d love to get rid of document.write in my scripts, but so far I haven’t found a satisfying alternative.
    I write widgets based on jQuery. The first step is to check if jQuery is already loaded on the page, and if not then load it:
    window.jQuery||document.write(<script…
    Asynchronous load is not acceptable as the widget depends on jQuery.