Load javascript async, then check DOM loaded before executing callback

17,014

What you need is a simple queue of onload functions. Also please avoid browser sniffing as it is unstable and not future proof. For full source code see the [Demo]

var onload_queue = [];
var dom_loaded = false;

function loadScriptAsync(src, callback) {
  var script = document.createElement('script'); 
  script.type = "text/javascript";
  script.async = true;
  script.src = src;
  script.onload = script.onreadystatechange = function() {
    if (dom_loaded) 
      callback();
    else 
      onload_queue.push(callback);
    // clean up for IE and Opera
    script.onload = null;
    script.onreadystatechange = null;
  };
  var head = document.getElementsByTagName('head')[0];
  head.appendChild(script);
}

function domLoaded() {
   dom_loaded = true;
   var len = onload_queue.length;
   for (var i = 0; i < len; i++) {
     onload_queue[i]();
   }
   onload_queue = null;
};

// Dean's dom:loaded code goes here
// do stuff
domLoaded();

Test usage

loadScriptAsync(
  "http://code.jquery.com/jquery-1.4.4.js", 
  function() {
      alert("script has been loaded");
   }
);
Share:
17,014
Ken
Author by

Ken

Technologist who's been a developer, sales engineer, and solutions architect. On the path to PM.

Updated on July 20, 2022

Comments

  • Ken
    Ken almost 2 years

    Problem:
    Load js files asynchronously, then check to see if the dom is loaded before the callback from loading the files is executed.

    edit: We do not use jQuery; we use Prototype.
    edit: added more comments to the code example.

    I am trying to load all of my js files asynchronously so as to keep them from blocking the rest of the page. But when the scripts load and the callback is called, I need to know if the DOM has been loaded or not, so I know how to structure the callback. See below:

    //load asynchronously
    (function(){
            var e = document.createElement('script'); 
            e.type = "text/javascript";
            e.async = true;
            e.src = srcstr; 
            // a little magic to make the callback happen
            if(navigator.userAgent.indexOf("Opera")){
                e.text = "initPage();";
            }else if(navigator.userAgent.indexOf("MSIE")){
                e.onreadystatechange = initPage;
            }else{
                e.innerHTML = "initPage();";
            }
            // attach the file to the document
            document.getElementsByTagName('head')[0].appendChild(e);
    })();
    
    initPageHelper = function(){ 
        //requires DOM be loaded
    }
    
    initPage = function(){
        if(domLoaded){ // if dom is already loaded, just call the function
            initPageHelper();
        }else{ //if dom is not loaded, attach the function to be run when it does load
            document.observe("dom:loaded", initPageHelper);
        }
    }
    

    The callback gets called properly due to some magic behind the scenes that you can learn about from this Google talk: http://www.youtube.com/watch?v=52gL93S3usU&feature=related

    What's the easiest, cross-browser method for asking if the DOM has loaded already?

    EDIT
    Here's the full solution I went with.
    I included prototype and the asynchronous script loader using the normal method. Life is just so much easier with prototype, so I'm willing to block for that script.

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

    And actually, in my code I minified the two files above and put them together into one file to minimize transfer time and http requests.

    Then I define what I want to run when the DOM loads, and then call the function to load the other scripts.

    <script type="text/javascript">
        initPage = function(){
        ...
        }
    </script>
    <script type="text/javascript">
        loadScriptAsync("scriptaculous/scriptaculous.js", initPage);
        loadScriptAsync("scriptaculous/effects.js", initPage);
        loadScriptAsync("scriptaculous/controls.js", initPage);
            ...
        loadScriptAsync("mypage.js", initPage);
    </script>
    

    Likewise, the requests above are actually compressed into one httpRequest using a minifier. They are left separate here for readability. There is a snippet at the bottom of this post showing what the code looks like with the minifier.

    The code for asyncLoader.js is the following:

    /**
     * Allows you to load js files asynchronously, with a callback that can be 
     * called immediately after the script loads, OR after the script loads and 
     * after the DOM is loaded. 
     * 
     * Prototype.js must be loaded first. 
     * 
     * For best results, create a regular script tag that calls a minified, combined
     * file that contains Prototype.js, and this file. Then all subsequent scripts
     * should be loaded using this function. 
     * 
     */
    var onload_queue = [];
    var dom_loaded = false;
    function loadScriptAsync(src, callback, run_immediately) {
          var script = document.createElement('script'); 
          script.type = "text/javascript";
          script.async = true;
          script.src = src;
          if("undefined" != typeof callback){
              script.onload = function() {
                    if (dom_loaded || run_immediately) 
                      callback();
                    else 
                      onload_queue.push(callback);
                    // clean up for IE and Opera
                    script.onload = null;
                    script.onreadystatechange = null;
              };
    
              script.onreadystatechange = function() {
                if (script.readyState == 'complete'){
                    if (dom_loaded || run_immediately) 
                      callback();
                    else 
                      onload_queue.push(callback);
                    // clean up for IE and Opera
                    script.onload = null;
                    script.onreadystatechange = null;
                }else if(script.readyState == 'loaded'){
                    eval(script);
                     if (dom_loaded || run_immediately) 
                          callback();
                    else 
                      onload_queue.push(callback);
                    // clean up for IE and Opera
                    script.onload = null;
                    script.onreadystatechange = null;
                }
              };
          }
          var head = document.getElementsByTagName('head')[0];
          head.appendChild(script);
    }
    document.observe("dom:loaded", function(){
        dom_loaded = true;
        var len = onload_queue.length;
        for (var i = 0; i < len; i++) {
            onload_queue[i]();
        }
        onload_queue = null;
    });
    

    I added the option to run a script immediately, if you have scripts that don't rely on the page DOM being fully loaded.

    The minified requests actually look like:

    <script type="text/javascript" src="/min/?b=javascript/lib&f=prototype/prototype.js,asyncLoader.js"></script>
    <script type="text/javascript"> initPage = function(e){...}</script>
    <script type="text/javascript">
        srcstr = "/min/?f=<?=implode(',', $js_files)?>";
        loadScriptAsync(srcstr, initPage);
     </script>
    

    They are using the plugin from: [http://code.google.com/p/minify/][1]

  • Ken
    Ken over 13 years
    this won't work, as i want my scripts to start loading asynchronously at the top of the page
  • rob
    rob over 13 years
    well, at the bottom you can add a script tag that simply sets a global variable "var loaded=true;" or the like. Or it could have an element with an id, and you can check for that element existing.
  • Ken
    Ken over 13 years
    but what if the async callback returns after the dom has loaded?
  • Ken
    Ken over 13 years
    the only problem with this update is that I also wanted to load the prototype library asynchronously, which means that document.observe() will not be available.
  • gblazex
    gblazex over 13 years
    that's no problem, just use dean.edwards.name/weblog/2006/06/again for dom:loaded, even jQuery uses a modified version of this.
  • Ken
    Ken over 13 years
    script.onload = script.onreadystatechange = function()... this function will be called twice in Opera. you have to set a flag and check that the function has not been run yet.
  • philgiese
    philgiese over 13 years
    And why do you want them to load at the top? If you just place them at the bottom, the parser will go over the html, load the images non-blocking and will then struggle with your javascripts while still showing the rest of the page. And as it seems, that your scripts need a loaded dom anyway, why fiddling with that? A simple $(document).observe("dom:loaded", function() {...}) will do the work.
  • gblazex
    gblazex over 13 years
    Actaully you don't have to. Just remove the event listeners by nulling them. It also prevents IE memory leaks! Updated the code + demo.
  • Ken
    Ken over 13 years
    Because once the dom is loaded, pieces of it can be displayed. I need the controls to be maximally responsive, and many of them are in js. So I want the js to transfer asynchronously at the same time as the markup, then i want the handlers attached as soon as the dom is ready to create maximum responsiveness. I don't want to wait to transfer the files until after the dom is loaded, and then attach the handlers.
  • rob
    rob over 13 years
    Makes sense, although I doubt you really buy anything by having the load initiated at the top of the page vs. the bottom (unless the page is huge and slow to generate). Regardless, you can do it by calling a function at the bottom of the page, and call the same function at the bottom of each javascript file that is loaded. Only the very last time the function is called will it actually trigger anything....all the other times it will be keeping a count. So it doesn't matter if the dom is loaded first, or the js files....the last time through it will trigger whatever behavior you need.
  • Kyle Baker
    Kyle Baker about 7 years
    this would not wait on the async scripts to finish loading. It would just be a sketchy proxy for DOMContentLoaded.