ControlJS part 1: async loading

December 15, 2010 10:45 pm | 30 Comments

This is the first of three blog posts about ControlJS – a JavaScript module for making scripts load faster. The three blog posts describe how ControlJS is used for async loading, delayed execution, and overriding document.write.

The #1 performance best practice I’ve been evangelizing over the past year is progressive enhancement: deliver the page as HTML so it renders quickly, then enhance it with JavaScript. There are too many pages that are blank while several hundred kB of JavaScript is downloaded, parsed, and executed so that the page can be created using the DOM.

It’s easy to evangelize progressive enhancement – it’s much harder to actually implement it. That several hundred kB of JavaScript has to be unraveled and reorganized. All the logic that created DOM elements in the browser via JavaScript has to be migrated to run on the server to generate HTML. Even with new server-side JavaScript capabilities this is a major redesign effort.

I keep going back to Opera’s Delayed Script Execution option. Just by enabling this configuration option JavaScript is moved aside so that page rendering can come first. And the feature works – I can’t find a single website the suffers any errors with this turned on. While I continue to encourage other browser vendors to implement this feature, I want a way for developers to get this behavior sooner rather than later.

A number of web accelerators (like Aptimize, Strangeloop, FastSoft, CloudFlare, Torbit, and more recently mod_pagespeed) have emerged over the last year or so. They modify the HTML markup to inject performance best practices into the page. Thinking about this model I considered ways that markup could be changed to more easily delay the impact of JavaScript on progressive rendering.

The result is a JavaScript module I call ControlJS.

Controlling download and execution

The goal of ControlJS is to give developers more control over how JavaScript is loaded. The key insight is to recognize that “loading” has two phases: download (fetching the bytes) and execution (including parsing). These two phases need to be separated and controlled for best results.

Controlling how scripts are downloaded is a popular practice among performance-minded developers. The issue is that when scripts are loaded the normal way (<script src=""...) they block other resource downloads (lesser so in newer browsers) and also block page rendering (in all browsers). Using asynchronous script loading techniques mitigates these issues to a large degree. I wrote about several asynchronous loading techniques in Even Faster Web Sites. LABjs and HeadJS are JavaScript modules that provide wrappers for async loading. While these async techniques address the blocking issues that occur during the script download phase, they don’t address what happens during the script execution phase.

Page rendering and new resource downloads are blocked during script execution. With the amount of JavaScript on today’s web pages ever increasing, the blocking that occurs during script execution can be significant, especially for mobile devices. In fact, the Gmail mobile team thought script execution was such a problem they implemented a new async technique that downloads JavaScript wrapped in comments. This allows them to separate the download phase from the execution phase. The JavaScript is downloaded so that it resides on the client (in the browser cache) but since it’s a comment there’s no blocking from execution. Later, when the user invokes the associated features, the JavaScript is executed by removing the comment and evaluating the code.

Stoyan Stefanov, a former teammate and awesome performance wrangler, recently blogged about preloading JavaScript without execution. His approach is to download the script as either an IMAGE or an OBJECT element (depending on the browser). Assuming the proper caching headers exist the response is cached on the client and can later be inserted as a SCRIPT element. This is the technique used in ControlJS.

ControlJS: how to use it

To use ControlJS you need to make three modifications to your page.

Modification #1: add control.js

I think it’s ironic that JavaScript modules for loading scripts asynchronously have to be loaded in a blocking manner. From the beginning I wanted to make sure that ControlJS itself could be loaded asynchronously. Here’s the snippet for doing that:

var cjsscript = document.createElement('script');
cjsscript.src = "control.js";
var cjssib = document.getElementsByTagName('script')[0];
cjssib.parentNode.insertBefore(cjsscript, cjssib);

Modification #2: change external scripts

The next step is to transform all of the old style external scripts to load the ControlJS way. The normal style looks like this:

<script type="text/javascript" src="main.js"><script>

The SCRIPT element’s TYPE attribute needs to be changed to “text/cjs” and the SRC attribute needs to be changed to DATA-CJSSRC, like this:

<script type="text/cjs" data-cjssrc="main.js"><script>

Modification #3: change inline scripts

Most pages have inline scripts in addition to external scripts. These scripts have dependencies: inline scripts might depend on external scripts for certain symbols, and vice versa. It’s important that the execution order of the inline scripts and external scripts is preserved. (This is a feature that some of the async wrappers overlook.)  Therefore, inline scripts must also be converted by changing the TYPE attribute from “text/javascript” in the normal syntax:

<script type="text/javascript">
var name = getName();

to “text/cjs”, like this:

<script type="text/cjs">
var name = getName();

That’s it! ControlJS takes care of the rest.

ControlJS: how it works

Your existing SCRIPTs no longer block the page because the TYPE attribute has been changed to something the browser doesn’t recognize. This allows ControlJS to take control and load the scripts in a more high performance way. Let’s take a high-level look at how ControlJS works. You can also view the control.js script for more details.

We want to start downloading the scripts as soon as possible. Since we’re downloading them as an IMAGE or OBJECT they won’t block the page during the download phase. And since they’re not being downloaded as a SCRIPT they won’t be executed.  ControlJS starts by finding all the SCRIPT elements that have the “text/cjs” type. If the script has a DATA-CJSSRC attribute then an IMAGE (for IE and Opera) or OBJECT (for all other browsers) is created dynamically with the appropriate URL. (See Stoyan’s post for the full details.)

By default ControlJS waits until the window load event before it begins the execution phase. (It’s also possible to have it start executing scripts immediately or once the DOM is loaded.) ControlJS iterates over its scripts a second time, doing so in the order they appear in the page. If the script is an inline script the code is extracted and evaluated. If the script is an external script and its IMAGE or OBJECT download has completed then it’s inserted in the page as a SCRIPT element so that the code is parsed and executed. If the IMAGE or OBJECT download hasn’t finished then it reenters the iteration after a short timeout and tries again.

There’s more functionality I’ll talk about in later posts regarding document.write and skipping the execution step. For now, let’s look at a simple async loading example.

Async example

To show ControlJS’s async loading capabilities I’ve created an example that contains three scripts in the HEAD:

  • main.js – takes 4 seconds to download
  • an inline script that references symbols from main.js
  • page.js – takes 2 seconds to download and references symbols from the inline script

I made page.js take less time than main.js to make sure that the scripts are executed in the correct order (even though page.js downloads more quickly). I include the inline script because this is a pattern I see frequently (e.g., Google Analytics) but many script loader helpers don’t support inline scripts as part of the execution order.

Async withOUT ControlJS is the baseline example. It loads the scripts in the normal way. The HTTP waterfall chart generated in IE8 (using HttpWatch) is shown in Figure 1. IE8 is better than IE 6&7 – it loads scripts in parallel so main.js and page.js are downloaded together. But all versions of IE block image downloads until scripts are done, so images 1-4 get pushed out. Rendering is blocked by scripts in all browsers. In this example, main.js blocks rendering for four seconds as indicated by the green vertical line.

Figure 1: Async withOUT ControlJS waterfall chart (IE8)

Async WITH ControlJS demonstrates how ControlJS solves the blocking problems caused by scripts. Unlike the baseline example, the scripts and images are all downloaded in parallel shaving 1 second off the overall download time. Also, rendering starts immediately. If you load the two examples in your browser you’ll notice how dramatically faster the ControlJS page feels. There are three more requests in Figure 2′s waterfall chart. One is the request for control.js – this is loaded asynchronously so it doesn’t slow down the page. The other two requests are because main.js and page.js are loaded twice. The first time is when they are downloaded asynchronously as IMAGEs. Later, ControlJS inserts them into the page as SCRIPTs in order to get their JavaScript executed. Because main.js and page.js are already in the cache there’s no download penalty, only the short cache read indicated by the skinny blue line.

Figure 2: Async WITH ControlJS waterfall chart (IE8)

The ControlJS project

ControlJS is open source under the Apache License. The control.js script can be found in the ControlJS Google Code project. Discussion topics can be created in the ControlJS Google Group. The examples and such are on my website at the ControlJS Home Page. I’ve written this code as a proof of concept. I’ve only tested it on the major browsers. It needs more review before being used in a production environment.

Only part 1

This is only the first of three blog posts about ControlJS. There’s more to learn. This technique might seem like overkill. If all we wanted to do was load scripts asynchronously some other techniques might suffice. But my goal is to delay all scripts until after the page has rendered. This means that we have to figure out how to delay scripts that do document.write. Another goal is to support requirements like Gmail mobile had – the desire to download JavaScript but delay the blocking penalties that come during script execution. I’ll be talking about those features in the next two blog posts.

var name = getName();

30 Responses to ControlJS part 1: async loading

  1. This looks really exciting, Steve! I’ve been playing with headjs and and considering putting it I to production, but I will have to try out this as well. I really liking the idea of using a simple library for performance.

    Hopefully we will get some technique cross-pollinization!

  2. I may be wrong, as I can’t test in IE right away, but wouldn’t this technique cause dual/simultaneous requests in IE (maybe other browsers).

    IE loads script elements with fake type attributes, but it doesn’t execute them. So with the mark up using the fake attributes, it seems like IE would start loading the script (without executing) and then controljs would load that same src into an image in order to preload it, likely before the first was done loading, and therefore, not a cache hit. Using small test javascript files may avoid seeing this race condition (if it really exists).

    The graphs posted here don’t seem to reflect this, anyone know why this wouldn’t be the case? Is IE loading fake types on script elements only for dynamically injected scripts?

    For other browsers that don’t support the loading of fake types, it seems like it would have the desired effect.

    Also, does anyone have a solution for Opera? None of these techniques work in Opera. The img hack _seems_ to work, but Opera calls the onerror handler as soon as it gets an incorrect type, so the callback is too early. I’d love to see a cross-browser loader that can preload without executing in Opera. (LABjs gets around this by inserting scripts in order, and Opera automatically executes them in insertion order. But, that means that they _have_ to be executed, which is not the same as an indefinite preload.)

    Thanks for the hard work and research Steve!

  3. Very exciting, indeed… although polling at 200 milliseconds for download completion seems inefficient. That’s potentially a lot of time wasted waiting in a timer. You can eliminate the need to poll altogether by adding another call to CJS.processNextScript() at the end of CJS.onloadCallback(). Processing order is maintained and the polling is no longer needed (in fact, you can just remove the entire “// still downloading” conditional branch).

  4. Just so I don’t confuse anyone, I went and tested and found that if you create images in Opera with `new Image()` instead of `document.createElement` you get better results.

  5. @Alex: I’m not seeing the fake types double load in IE (but have only tested on IE8). These examples work on Opera (tested on 10.63). Maybe clear your cache to get the bug fixes for control.js. If you experience errors in Opera please let me know (a bug report would be great).

    @Ray: Thanks for the detailed analysis. I’ll take a look.

  6. Steve,

    Very interesting. I ran the baseline and ControlJS through the Android browser and generated the waterfall performance charts for you. You can find them here: Both are a virtual dead heat, however with ControlJS you get twice as much data downloaded in the same time.


  7. @Alex — I thought the same at first (about IE and double-loads). Steve recommends changing the `src` attribute to `cjssrc`, so the browser doesn’t know anything about this script tag (basically ignores it), therefore no extra load.

    Of course, this means that the script not only has an invalid type, but also an invalid attribute name. Invalid markup is not, in my opinion, the way to address these problems.

    I wish more attention was given by Steve (and others) to my efforts with the W3C to standardize a real solution that addresses async, non-blocking loading AND even the document.write() use case. My `async=false` proposal (now implemented in FF4 nightly and soon in Webkit nightly) does that.

  8. @Steve-
    Some observations about CJS:

    1. You are relying on user-agent sniffing. This is simply something I cannot support. LABjs reluctantly uses some browser inferences (but which are distinctly more reliable than user-agent sniffing). Those browser inferences are necessary now, but will be removed as soon as browsers give a better option.

    We all know that feature testing is the right way forward. My async=false proposal allows proper feature testing going forward. I think this is a much better future-proof solution. I’d rather see the efforts in that direction rather than regressive behavior like bad-practice user-agent sniffing.

    2. The whole reason LABjs even added the extra complexity of the XHR preloading of local scripts is because *you* said (during LABjs’ original development cycle) that as much as 70% of script resources on the internet are served with improper (or no) caching headers.

    Has that balance radically changed since then? I haven’t seen research that shows that all (or even most) scripts are now being served with proper caching.

    The problem with controljs (and many other loaders like it) is the assumption that you can preload a resource into the cache and then re-request that element from cache without a double-load.

    I’m confused why you’re now relying on that assumption, when it was the single biggest sticking point for you when I originally shared LABjs’ techniques back in fall of last year? The preload-cache trick in LABjs was always the ugly fallback that would eventually be deprecated and removed, but you are making it the forefront technique for your loader.

    If people need to load a script from a location they don’t control and it is not properly cache-header serving, CJS will cause a double-load (and it will break the immediate-execute-from-cache assumption with a race condition). Is your suggestion for them that they not use CJS on such pages?

    3. How does CJS work if a user doesn’t convert *all* their script tags to the CJS style? In other words, if I leave one of my scripts untouched by CJS because of the above point #2, but the other scripts before and after it in the markup I do attach CJS to, won’t that completely break the relative ordering of the CJS and non-CJS loaded scripts?

  9. @Kyle

    You wrote: “The problem with controljs (and many other loaders like it) is the assumption that you can preload a resource into the cache and then re-request that element from cache without a double-load.”

    Why is that assumption wrong or bad?
    Stoyan’s technique for preloading JS files without executing them, resulting in the file being in the browser cache, works fine. On subsequent requests, the file may be used from cache.

    Whether or not the browser may use it from cache directly or after a conditional request to the server, has nothing to do with ControlJS. That is dependent on the response headers that were sent along with the file at first download.

    You being the request/response header master (re: your Perf Calendar post), please explain further how that assumption is wrong and/or my reasoning here.

  10. @Aaron – the reason it’s a bad assumption is that a lot (steve once said 70%) of scripts on the web are sent with improper cache headers or none at al. The reasons for this are varied, frustrating, and unfortunate, but it’s nonetheless a reality.

    If you use such a technique on a script that doesn’t cache properly:
    1) it’ll cause a double-load of that script when you request it the second time
    2) there’s a second assumption, which is that a script in cache will pull and execute nearly immediately. Of course, if it’s not in cache, it’ll be a second full (and non-immediate) load…but the lib assumes the script executed, and moves on, causing a race condition.

    Even if (2) is abated by waiting for the load event the second “request”, which many script loaders don’t do, (1) causes a costly double-load.

  11. Not that it matters much, but from conformance point of view, shouldn’t it be data-cjssrc?

  12. @Kyle

    right. Browser downloads file, may not store it in cache/re-use from cache, so when ControlJS puts it into the DOM, the file is downloaded again.

    - only use ControlJS with JS files that you serve yourself (= full control over headers).
    - If you really want to use a JS file from a 3rd party server, check the caching policy (what headers are sent). If that looks positive, you may decide to use it with ControlJS.
    But, it’s risky to use ControlJS with JS files served from 3rd party servers, because if they change the caching policy, your visitors may get a slower page/sluggish experience (double download delay).

    Imo, it’s always risky to use 3rd party served JS on your own site (their server down/slow = your users a bad experience).

  13. Great technique, we’ve adopted a similar angle and are very happy with it.

    A rather simple way to handle non-cacheable files is to extend CJS to support running a script without preloading it. These scripts can be especially marked (e.g. cjspreload=0), and the preload code can ignore those.

    Evaluating these scripts will then be done in a sequential manner, but still won’t block the rest of the page.

    A key value prop of CJS is to allow you to use 3rd party without slowing down your page…

  14. @Guypo –
    that would significantly complicate the functionality of sequencing all the scripts together with inline scripts. for instance, would such “non-preloaded” scripts still need to execute in the same order in the markup as they would have if they had been preloaded?

    if so, then, you’re going to make the other scripts after it wait for that script to completely load and execute before continuing. parallel loading is a really important part of dynamic script loaders’ functionality. if you lose parallel loading, you’re actually getting WORSE performance than if you’d just used normal script tags with no loader at all, because all modern browsers will parallel load a bunch of scripts and execute them in proper order.

    if not, then such non-preloaded scripts are going to be in a separate “execution queue” independent of the preloaded ones, and then you’ve made the loading order a lot less sensible from the markup perspective.

    fwiw, LABjs “preloads” scripts using a few tricks, to ensure this parallel loading with serial execution. for local scripts, it uses XHR (not subject to the uncacheable double-load problem).

    only for remote domain scripts does LABjs reluctantly fall back on this potentially dangerous “preload” trick that may cause double-load. but controljs uses this technique as its primary approach, meaning its subjecting its users to more possible hazard.

    and your only answer for that scenario is to lose parallel loading? that defeats almost the entire purpose of the loader.

  15. Steve I thought what happends if CJS.start is excecuted before all script tags are in DOM? This could happen if control.js is in cache and the page uses flush. However the 2nd call to findScripts works excellent.


    Well done! thanks for share

  16. @Kyle: I’ll answer your questions, but I’d like to remind everyone that the point of ControlJS is to propose a new attitude toward loading scripts – more awareness of the execution phase, the pain points of that phase, and ways to better control that phase. Many of your questions are implementation details that distract from the main conversation, but let me address them anyway.

    I’m not so religious about browser sniffing. If this is a big deal to you it’s easy to workaround – we could do both the image and the object and do both script.onload and script.onreadystatechange and add code to avoid double work. Again, this is code detail that I’m not focusing on. It’d be great if you wanted to add those details to the code.

    The 70% stat is also somewhat irrelevant. As Aaron points out, the developer can decide to change their headers, opt out of ControlJS if that’s not possible, or charge ahead regardless. In the latter case, adding “cjspreload=0″ as Guypo suggests would avoid double downloads. You mention “the lib assumes the script executed, and moves on”. Early this morning I added an execution queue that solves this issue, so even if a script has to be downloaded during the execution phase everything still works (execution order is preserved).

    Even though the status of the headers is somewhat a distraction, I wanted to clarify that stat. I think you’re misremembering the 70% stat – that stat has to do with what percentage of users hit websites with an empty vs. primed cache. I just did an analysis of scripts loaded by the top 20K websites wwide – 38% of these scripts don’t have any expiration info, 49% have expiration headers greater than 20 seconds. But again, the developer can address this issue by adding caching headers or the “cjspreload=0″ idea.

    Since your point #2 is moot, point #3 isn’t an issue.

    Great questions, but I’m mostly interested in the value people find in getting better progressive enhancement. All the script loaders out there now force the browser to parse and execute code during page loading regardless of whether the code is needed immediately. I think there’s value in getting pages to render more quickly for a better user experience.

  17. @edvakf: I apologize but I’m not up to speed on validation. Can you point to a reference about why data-cjssrc is better? Are there similar validation issues with type=”text/cjs”?

    @Martin: Yes, early this morning I added the 2nd pass to CJS.findScripts to address the situation you describe. ;-)

  18. @Stever, so the thing with the data-cjssrc is to find a projected valid way of introducing a new attribute. In HTML 4 or XHTML 1.x, “cjssrc” will lead to the page being invalid and a lot of us still like/insist on validity as a way of detecting errors ahead of sending out content.

    In HTML 5, the proposal (i.e. it’s in the spec, but HTML 5 is still a WIP) is that any attribute which starts with “data-” is considered private in the sense that the browser won’t use it for rendering or otherwise act on it, but it will be available in the DOM for scripts to use. So it’s the recommended way of adding new stuff like this. As browsers roll out support for HTML 5, it’s probably worthwhile adopting that convention straight away to be forwards compatible.

    The relevant proposal is at

  19. @Steve-
    First of all, I think it’s a flawed assumption that to accomplish your goal(s), you needed a whole new loader.

    I could (and do in some of my sites!) *easily* do a setTimeout() (or attach to window.onload, as you do) to have my LABjs script loading chain not start until after the page’s content has fully loaded.

    In 2 or 3 simple lines of logic, I can defer LABjs to happen until after the content arrives, and I think it would have pretty much solved the main pain point you’re dealing with.

    No, it wouldn’t solve document.write(), but that’s a feature of the web that I’ve been calling for to die for awhile, so I’m not really that concerned with it, to be honest.

    You say you’re more concerned with the dialogue around your different view of how page-load optimization should work — that you think all (or most) scripts shouldn’t be loaded until after all content has been loaded.

    OK. I have a good long discussion (section “ControlJS: backstory) on this blog post that I just published about my feelings of the (negative) UX impact your ideas may have for a lot of sites.

    Whether the caching stat is 70% or 38% or 51% or somewhere in between, I think the concern that you had back then, and that I still have now, is valid. It’s still a pretty big chunk of the web (unignorably big) for whom such loading techniques are going to be (possibly much) more complicated. I am not willing to concede that it’s a moot detail yet. Apparently you have.

    As I asserted to @Guypo, if you propose that two or more scripts that aren’t cacheable must be loaded serially (not in parallel), you’ve completely defeated the purpose of a script loader. As you pointed out to me a long time ago, that would have the effect of being WORSE performance than if the author had just left them as two markup script tags and let the browser handle loading them, because the modern browsers will do so in parallel but with proper execution order (regardless of cacheability).

    Bottom line: if you’re trying to convince people to use ControlJS as a general script loader, you have to consider the “lowest common denominator” and the fact that a LOT of people use script tags in their pages without understand caching or headers at all.

    How can we expect that for a mom-and-pop site (the majority of the web) to use a script loader library (which should be a simple and smooth transition as much as possible), they have to learn and consider all kinds of extra details like headers, caching, and the like. Worse yet, they may have to figure out how to create custom logic that handles CJS loading some of their code (that is cacheable) and other ad-hoc loading code that isn’t cacheable.

  20. @steve

    This is really great. Do you recommend to move the google analytics async code over to controljs ?

  21. @Malcolm: Great explanation. I’ve added this to the list of Issues (

    @Quentin: I would not use ControlJS for GA because 1) GA already has an async snippet, 2) we want the GA beacon to fire quickly before the user leaves the page (before onload fires), and 3) the amount of code is small.

  22. @Kyle

    in comment #19 you write how it would be easy to defer LABjs until after the content has loaded.
    Attaching it to window.onload is easy, but the downside is that the scripts don’t start loading before the load event. That is pretty late.
    The setTimeout() is easy to and has the benefit of scripts starting to download before the load event, but this also means that the *execution* of the scripts

    So my question is: can LABjs do this:
    a) *download* scripts asap
    b) start *executing* the scripts after DOMContentLoaded or onload

    The benefit of this is that the scripts are downloaded soon and only the execution takes place later. If you simply defer LABjs until after onload, the scripts need to download, parse & execute and in that case there is a bigger chance of the UX not being optimal. E.g. visitor uses search box and the JS for the autocomplete is not yet ready.

    If this can be done with LABjs, please be so kind to create a Gist with a code example and post the link here.

  23. @Aaron-
    At present, this is not a use-case that LABjs can serve. There’s no “delay” between when a script is loaded and when it’s executed, except for if it’s waiting on previous scripts to execute first.

    I know that’s kind of the use-case that Steve’s going for with CJS. Honestly, to me, it seems a little overkill. I think it’s far more impactful *when* you have your 100k of JS *loading*, clogging the tubes — in other words, are you taking up bandwidth during initial load for the JS, or are you letting all content load quickly first.

    I guess I just don’t see how, in normal desktop context, the 100-200ms of JS execution time is really what’s contributing to such bad page-load times/experience. I personally think the download part is the much more critical issue to be tackling.

    But I can see how it might be helpful in the mobile case, similar to how Gmail mobile did it with the /* .. */ trick.

    I suppose that it wouldn’t be too difficult to add a feature like this to the LABjs API. I’d probably model it like setting a `defer` property on the chain, which says to defer execution (but not loading) of all scripts until later. I’ll consider that for the next (v2.0) release of LABjs.

  24. FWIW, I’ve started some discussion threads on W3C public-html, which I encourage readers to follow (and join!):

    1. How should resources be loaded/cached when used in the hacky <object/new Image() technique:

    2. Can we spec a direct facility for “preloading” that allows on-demand control of the parsing/execution separate from the loading:

  25. ControlJS now uses data-cjssrc and data-cjsexec as the SCRIPT attributes.

  26. Hi Steve,
    What will happen if there is a image node like this.

    Is there a way we can avoid such cases without changing the code?

  27. Some how my previous comment is not posted completely.
    What will happen if there is a image node with onload handler?.

  28. @Satyanarayana: It’s better to attach handlers with addEventListener vs HTML attributes.

  29. Great Job steve!
    I’ve just got one problem… I don’t get google scripts like show_ads.js or the costum-search run with ControlJS.
    neither with data-cjssrc nor with just src.
    It works when i put the google scripts in front of the ControlJS initialisation.
    But i’m wondering why it doesn’t work even if i don’t use ControlJS to load my google code…
    Do you have some advice for me?

  30. @Christopher: Contact me and provide the usual bug info (including test page URL) and we’ll debug this offline.