Skip to content

Latest commit

 

History

History
393 lines (323 loc) · 16.6 KB

README.markdown

File metadata and controls

393 lines (323 loc) · 16.6 KB

defer.js

Async Everything

defer.js is a tiny boot-strapper that allows you to make all loading of JavaScript on your page asynchronous. Yes, everything.

Packed in less than 2kB (gzipped), defer.js is a predicate-based execution engine. You give it a rule, and once the condition is met, any hunk of JS you've got will be run. It's that simple.

Using predicates to let all your JS load asynchronously is just a welcome side-effect.

An example:

defer( {
    predicate:  function(){ return condition === true; } ,
    handler:    function(){ runThisCode(); } ,
    options:    {}
} );
// you can load the external script you just referenced, even after-the-fact

Huh?

  • Load any script you'd like asynchronously, even libraries like jQuery
  • With all-async code, the 'meat' of your page will load faster, especially on mobile devices. Text and images don't have to wait for scripts before loading.
  • Put code on the page where you need to, even if that's before your libraries!

Why async & deferred JS is important

Make pages feel faster.

Loading your code asynchronously allows your browser to load every resource it can as quickly as possible, often in parallel. This includes images, stylesheets, etc. It also means no blocking — your browsing experience needn't wait for code you may not need yet.

Google's Page Speed documentation explains the benefits of asynchronous and deferred loading in more technical detail.

Origin

A year ago, I was considering how Google Analytics stores data in-page before its own code had loaded. If you've never seen it before, it looks like this:

var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-XXXXXXX-X']);
_gaq.push(['_trackPageview']);
	
(function() {
    var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
    ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
    var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();

By cleverly abusing an array, they store your configuration information while only using a single, oddly-named global variable. Not bad.

On a separate tangent, I had stuck in my head a problem that popped up in a recent project. The site in question broke various page elements into components. Sometimes, data was fetched in the execution of these components, and needed to be passed to JS on that page. Someone proposed that we load the data via AJAX, but that seemed horrible. We didn't need a second HTTP roundtrip, or a second fetch of data from the DB. Data attributes weren't (and still aren't) sufficiently widespread to be of use. We threw the data in a custom object, in-page, referenced later by other code and called it a day.

We had wasted a global variable (not ideal), but it worked. Of course, this only worked for storing simple data, unless the massive site code got loaded first. Doing so isn't great for performance.

I wanted a way to defer executing that code until the framework code it depended on was good and ready. To make it universal, a custom function's return value could be used as the predicate.

The first draft of defer.js was all about predicates. Then, I realized we could use those facilities to let defer.js load itself.

##Wait, it loads itself? ##

More or less, yes.

defer.js uses a single global variable for everything: window.defer. If this code has been loaded, you can use it just as in the example above. If not, you can still use it right now.

defer.js uses a variant on the abuse-an-array idea, but to a higher degree. Once defer.js loads, it runs all the things you've put into the array, then it becomes the array.*

<script type="text/javascript">
    window.defer = window.defer || [];                    // in case defer.js hasn't loaded yet
    window.defer.push( {                                  // oddly familiar
        p:    function(){ return condition === true; } ,
        h:    function(){ runThisCode(); } ,
        o:    {}
    } );
</script>
<script type="text/javascript" async="async" src="defer.js"></script>

Yes, that totally works.

* JS doesn't offer operator overloading, so defer.push() is the only way in which you'll interact with defer.js like an array. None of that bracket business.

Note that we use window.defer instead of simply defer, since this is more reliable inside of closures, and in certain browsers.

Also note the use of single-character shorthands for predicate, handler, and options. You're welcome.

Are you ready?

defer.js sure is. It loads code as soon as the DOM is manipulable, much like jQuery's $.ready().

Out of Order?

By the time you ask the browser to evaluate any JavaScript, any other code it depends on must already be loaded. In a pre-asynchronous JS world, knowing what to load and when was easy enough (unless you had little control over the page's overall HTML). When you use use async out-of-the-box, things aren't so easy. It's tougher to know which will load first, a given chunk of code or the code it depends on.

If your dependent code tries to load first, you'll get a nasty error and likely a broken page. This out-of-order error is a race condition. With defer.js, it's also a thing of the past.

As long as your predicate logic is sound, you'll never again run afoul of an async-based race condition. At worst, it'll timeout (which you can adjust, and indicates issues in some other part of your infrastructure). A nice side-effect of defer.js's failure handling is that, for the first time, you can recover from code loading failures without the use of XMLHttpRequest.

Compatibility?

Yes.

If it's a browser, it'll work. defer.js has been tested on every browser I can get my mitts on, even those that don't support asynchronous loading of JS. In those cases, it does no harm.

That said, I assume no liability for the use of defer.js. Read the license for more details.

Resources

Besides its sub-2kB download size, defer.js is very respectful of browser resources. defer.js only runs when you ask it to, and does not leave any timers or event handlers 'dangling' thereafter.

As with all static scripts, be sure to set your cache-related headers correctly. Even 1kB is 1kB too much when it could be in cache.

Try it out

Want to see it in action, against loading an image and jQuery? Test defer.js now!

Pay special attention to the image load time & order. Note that the load times for both defer.js and jQuery in these test pages are based on the DOM being ready. Use Webkit Inspector's Timeline or your browser's equivalent to verify the numbers yourself.

Judiciously clearing your cache, along with manipulating your connection (I use Network Link Conditioner) will give you a more complete picture of defer.js's performance.

The benefits of defer.js are most pronounced on real-world-sized pages, and on slower network links (like mobile).

Methods

defer( {p:... , h:... , o:...} )
Use this to create a new item to defer. Returns a unique sequence ID number.
defer.push( {p:... , h:... , o:...} )
The asynchronous way to defer your code, as if you were adding to an array.
defer.cancel( sequenceID )
Pass in the sequence ID from the non-asynchronous defer() method, and you can cancel a deferred block of code. This is not guaranteed to prevent execution, that depends on the runloop's whims.
defer.version()
Returns a string of defer.js's version.
defer.isReady()
Returns a boolean indicating whether the DOM is ready for manipulation.

Options

options: { timeout: 30000 , interval: 100 , onFail: function(){ alert("failed"); } }
timeout
How long, in milliseconds, until defer.js gives up on running your handler. The default is 15000ms (15 seconds).
interval
How long, in milliseconds, between attempts to validate your predicate. The default is 50ms.
onFail
Your very own custom failure handler. If deferred your code within a closure, you can use your own references and variables in here. Note that this isn't safe to use here.

Sugar

There's more?

defer.js uses a few small helper functions, which have been externalized for your convenience.

defer.log()
This is a safe way to reference console.log, even in browsers that don't support the console. If you're using a production version of defer.js, your logs go straight to \dev\null. If you use the debug version, errors will route correctly to the browser's JS console. Easy.
defer.isNil( objToTest )
Test if the first parameter is undefined or null. Returns false if it's a dud.
defer.isFunction( functionToTest )
Test if the first parameter is a usable function. Returns false if not.
defer.forOwnIn( context , dictionary , handler )
Use this for testing every property on a custom dictionary/hash object without typing the whole hasOwnProperty stuff. The context is this, and dictionary is your object. The function you pass to handler has two parameters, key and value. Example:
    defer.forOwnIn( 
        this ,                                        // context
        { 'key1' : 'val1' , 'key2' : 'val2' } ,       // dictionary
        function( key , value ){
            defer.log( key + "=" + value );
        }
    );
defer.appendScript( src , async )
If you want to load code asynchronously, in code, use this. It simply attaches a <script> tag to the end of the page's body. The async parameter accepts either async or defer.

Compared To...

I looked at a few projects with vaguely similar aspirations before starting this project. I didn't want to re-invent the wheel.

Nothing I found had the combination of super-small load size, conceptual simplicity, zero dependencies, and pervasive asynchronicity.

Since then, a number of others JS loaders have popped up. Here's a little comparison:

Loader min+gzip min+gzip+license Async Itself Dependency Ordering Dependency-Free JS-Free Load
defer.js 1.4kB 1.8kB Yes! Yes (use predicates) Yes! Yes!
bdLoad 3.7kB 4.7kB No Yes Yes No
Bootstrap 0.7kB 1.1kB No No Yes No
BravoJS 6.5kB 7.2kB No Yes Yes No
Curl.js* 3.5kB 4.3kB No Yes Yes No
ControlJS 1.9kB 2.2kB No Yes Yes No
dominatejs 11.4kB 12kB No Yes Yes No
head.js 1.3kB 2.1kB No Yes Yes No
JSL 0.8kB 1.5kB No Yes Yes No
JSLoad 1.1kB 1.6kB No Yes Yes Yes
jQl 0.8kB 1.3kB Sort of Yes Yes No
LABjs 2.3kB 2.9kB Sort of Yes Yes No
LazyLoad 1kB 1.7kB No Yes Yes No
NBL 0.6kB 1.3kB No Yes Yes No
RequireJS 5.5kB 6.2kB No Yes Yes No
$script.js 0.9kB 1.5kB No Yes Yes No
Steal 4kB none specified No Yes Yes No
yepnope.js 1.7kB 1.7kB No Yes Yes No
YUI 2 Loader 9.9kB 10kB No Yes Yes No
YUI 3 Seed 20.7kB 20.7kB No Yes? Yes No

Honestly, I had no idea many of these loaders existed until I was revising this README. My thanks to Simone D'Amico and Éric Daspet, whose lists of JS loaders inspired the table above.

defer.js doesn't offer the kind of extensive dependency ordering that a lot of the others offer, for two reasons. First, it offers a new way around the async loading race condition that makes these other systems necessary. Second, defer.js exists to speed loading on the majority of web pages, and is not optimized for the Photoshop-in-the-browser class of web applications (though it ought to be of good use there, too!).

Regarding the filesizes listed: Some weren't minimized, so I minimized them using the Closure Compiler, at the highest level of optimization that wouldn't throw warnings or errors. Also, quite a few projects didn't include valid license boilerplate, so I included it where necessary. Specifically, MIT- and GPL-licensed code requires at least the boilerplate attached to each and every file. Persuant to these licenses, you technically have no rights to use the code without the proper boilerplate attached. (I am not a lawyer.)

License, Copyright & Trademark

defer.js is made available under the Apache License, Version 2.0.

All code in this repository is copyright 2011-2012 Ian J. Wessman.

"defer.js" and the defer.js logo are trademarks of Ian J. Wessman.