Preload script without execute

16,307

Solution 1

You should have a look at the following links:

http://calendar.perfplanet.com/2011/lazy-evaluation-of-commonjs-modules/

http://tomdale.net/2012/01/amd-is-not-the-answer/

And at how ember.js is using a tool called minispade and preprocessing with ruby to make the process of loading, parsing and running javascript modules fast.

Solution 2

With similar technique you may preload scripts and stylesheets using img for Internet Explorer and object tag for every other browser.

var isMSIE = /*@cc_on!@*/false;

var resources = ['a.js', 'b.js', 'c.css'];

for (var i=0; i<resources.length; i++){
  if (isMSIE){
    new Image().src = resources[i];
  } else {
    var o = document.createElement('object');
    o.data = resources[i];
    document.body.appendChild(o);
  }
}

There is a blog post describing such a technique and outlining caveats: Preload CSS/JavaScript without execution.

But why don't you want to just use dynamically added scripts just like suggested in other answer, this will probably lead to a cleaner solution with more control.

Solution 3

You can use the prefetch attribute of a link tag to preload any resource, javascript included. As of this writing (Aug 10, 2016) it isn't supported in Safari, but is pretty much everywhere else:

<link rel="prefetch" href="(url)">

More info on support here: http://caniuse.com/#search=prefetch

Note that IE 9,10 aren't listed in the caniuse matrix because Microsoft has discontinued support for them.

More info here and more options for preloading, like prerender and more

Solution 4

For each script you'd like to download without executing, make an object containing a name and a url, and put those objects into an array.

Looping through the array, use jQuery.ajax with dataType: "text" to download your scripts as text. In the done handler of the ajax call, store the text content of the file (which is passed in first argument) in the appropriate object, increment a counter, and call an "alldone" function when that counter is equal to the number of files you are downloading in this manner.

In the "alldone" function (or later) do the following: Loop through your array again, and for each entry, use document.createElement("script"), document.createTextNode(...), and (...scriptNode...).appendChild(...) to dynamically generate scripts having the intended source inline, rather than via "src" attribute. Finally, do document.head.appendChild(...scriptNode...), which is the point when that script is executed.

I have used this technique in a project where I needed to use frames, where several frames and/or the frameset need identical JavaScript files, in order to make sure each of those files is requested only once from the server.

Code (tested and working) follows

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">
<html>
    <head>
        <script id="scriptData">
            var scriptData = [
                { name: "foo"    , url: "path/to/foo"    },
                { name: "bar"    , url: "path/to/bar"    }
            ];
        </script>
        <script id="scriptLoader">
            var LOADER = {
                loadedCount: 0,
                toBeLoadedCount: 0,
                load_jQuery: function (){
                    var jqNode = document.createElement("script");
                    jqNode.setAttribute("src", "/path/to/jquery");
                    jqNode.setAttribute("onload", "LOADER.loadScripts();");
                    jqNode.setAttribute("id", "jquery");
                    document.head.appendChild(jqNode);
                },
                loadScripts: function (){
                    var scriptDataLookup = this.scriptDataLookup = {};
                    var scriptNodes = this.scriptNodes = {};
                    var scriptNodesArr = this.scriptNodesArr = [];
                    for (var j=0; j<scriptData.length; j++){
                        var theEntry = scriptData[j];
                        scriptDataLookup[theEntry.name] = theEntry;
                    }
                    //console.log(JSON.stringify(scriptDataLookup, null, 4));
                    for (var i=0; i<scriptData.length; i++){
                        var entry = scriptData[i];
                        var name = entry.name;
                        var theURL = entry.url;
                        this.toBeLoadedCount++;
                        var node = document.createElement("script");
                        node.setAttribute("id", name);
                        scriptNodes[name] = node;
                        scriptNodesArr.push(node);
                        jQuery.ajax({
                            method   : "GET",
                            url      : theURL,
                            dataType : "text"
                        }).done(this.makeHandler(name, node)).fail(this.makeFailHandler(name, node));
                    }
                },
                makeFailHandler: function(name, node){
                    var THIS = this;
                    return function(xhr, errorName, errorMessage){
                        console.log(name, "FAIL");
                        console.log(xhr);
                        console.log(errorName);
                        console.log(errorMessage);
                        debugger;
                    }
                },
                makeHandler: function(name, node){
                    var THIS = this;
                    return function (fileContents, status, xhr){
                        THIS.loadedCount++;
                        //console.log("loaded", name, "content length", fileContents.length, "status", status);
                        //console.log("loaded:", THIS.loadedCount, "/", THIS.toBeLoadedCount);
                        THIS.scriptDataLookup[name].fileContents = fileContents;
                        if (THIS.loadedCount >= THIS.toBeLoadedCount){
                            THIS.allScriptsLoaded();
                        }
                    }
                },
                allScriptsLoaded: function(){
                    for (var i=0; i<this.scriptNodesArr.length; i++){
                        var scriptNode = this.scriptNodesArr[i];
                        var name = scriptNode.id;
                        var data = this.scriptDataLookup[name];
                        var fileContents = data.fileContents;
                        var textNode = document.createTextNode(fileContents);
                        scriptNode.appendChild(textNode);
                        document.head.appendChild(scriptNode); // execution is here
                        //console.log(scriptNode);
                    }
                    // call code to make the frames here
                }
            };
        </script>
    </head>
    <frameset rows="200pixels,*" onload="LOADER.load_jQuery();">
        <frame src="about:blank"></frame>
        <frame src="about:blank"></frame>
    </frameset>
</html>

other question closely related to above approach other related question

Share:
16,307

Related videos on Youtube

Martin Borthiry
Author by

Martin Borthiry

Fan of Force.com platform. Developer and Administrator certified. I'm WPO (Web Performance Optimization) fan. I love improving frontend performance on webapps. Javascript Ninja :) keep it simple and keep it fast

Updated on September 15, 2022

Comments

  • Martin Borthiry
    Martin Borthiry about 1 year

    Problem

    In order to improve the page performance I need to preload scripts that I will need to run on the bottom page.

    I would like to take control of when the script is parsed, compiled and executed.

    I must avoid the script tag, because it is blocker for common render engines (geeko, etc).

    I can't load it using defer property, because I need to control when the script is executed. Also, async property is not a possibility.

    sample:

    <html><head>
    //preload scripts ie: a.js  without use the script
    </head><body> ..... all my nice html here
    //execute here a.js
    </body></html>
    

    This allows me to maximize the render performance of my page, because the browser will start to donwload the scripts content, and it will render the page at the same time in parallel. Finally, I can add the script tag, so the browser will parse, compile and execute the code.

    The only way that I could do that is using a hidden image tag. (This is a simplified version of Stoyan)

    i.e.

     <html><head>
     <img src="a.js" style=display:none;>
    </head><body> ..... all my nice html here
     <script src="a.js">  
    </body></html>
    

    Question

    I didn't find any problem using this technique, but does anyone know a better way to do this? Is there any meta prefetch?

    Additional information

    I'm using requirejs, so I'm trying to preload the modules code, without executing it, because this code depends of DOM elements.

  • Martin Borthiry
    Martin Borthiry over 11 years
    thanks, I knew that technique, but I need to control when the script is parsed and executed.
  • Martin Borthiry
    Martin Borthiry over 11 years
    Yea @Juicy, It seems to be the best approach to solve the problem. My version is a simplified copy of that script. Why no use dynamically added scripts? because, I would like to take control over when the script is parsed, compiled and executed. Do you know why is better use objects instead images on Non-IE browsers?
  • Martin Borthiry
    Martin Borthiry over 11 years
    Nice articles, I'm doing something similar to lazy-evaluation,but in order to reduce the amount of http requests I have all modules in one optimized file. It is similar to this sample: Files = {}; Files['main.js'] = "require(\"controllers/app_controller.js\");"; Files['controllers/app_controller.js'] = "alert(\"Hello world!\");";
  • txominpelu
    txominpelu over 11 years
    Then maybe you should consider rake pipeline. There's a project skeleton where they use minispade and rake papeline to generate one optimized file here: github.com/interline/ember-skeleton
  • dalore
    dalore over 8 years
    Note this advice is old and doesn't work. Try and load a script tag with an object when the url has a xframe options of sameorigin and it breaks. Move it to image and it works. The issues listed in that post (it won't cache in FF, don't seem to apply anymore0
  • Varis Vītols
    Varis Vītols about 5 years
    Prefetch is only useful for the next page, not for current one. Therefore, if you suddenly need an additional JS module in your single-page app, it won't be available, and will need to be downloaded anyway.
  • Varis Vītols
    Varis Vītols about 5 years
    I tried this approach, but I had issues with it. I have an Ember app, which consists of about 150 separate files, and with this many objects on the page, browser becomes extremely slow. Unfortunately, no-go for me.
  • Admin
    Admin about 4 years
    Maybe this is of additional help? phpied.com/preload-cssjavascript-without-execution