The eclectic musings of a bitter software engineer.

With all the fancy JavaScript libraries to choose from these days, it can be hard to keep your website's page weight down. Even if you're using a small, sleek library like YUI or MooTools, there are often times when it would be nice to be able to load certain scripts only when they're needed.

For example, if you load a bunch of fancy animation libraries to do something spiffy when the user clicks a button, but then the user does something else instead of clicking the button, then you've wasted precious time loading scripts you didn't even need.

Lazy loading is when you wait to load something until you actually need it. It's not a new concept, and the techniques for implementing lazy loading in JavaScript have been around for a while, but it can be a real pain in the ass to roll your own lazy loading solution, especially if you want it to be compatible with all the major browsers.

Luckily, I've done the hard work for you. All you need to do is include LazyLoad in any web page like so:

<script src="http://wonko.com/js/lazyload/1.0.3/lazyload-min.js"></script>

Then, when you need to load a script, call LazyLoad.load() or LazyLoad.loadOnce() and pass in the URL of the script you want to load (or an array of URLs if you need to load multiple scripts). You can also specify a callback function that will be executed when the scripts are finished loading.

The only difference between LazyLoad.load() and LazyLoad.loadOnce() is that loadOnce() checks to see if the scripts have already been loaded and makes sure nothing gets loaded twice.

// Called when all scripts have finished loading.
function loadComplete() {
  alert('Hooray for LazyLoad!');
}

// Load the YUI Dom, Event, and Animation libraries if they haven't already
// been loaded.
LazyLoad.loadOnce([
  'http://yui.yahooapis.com/2.2.2/build/yahoo-dom-event/yahoo-dom-event.js',
  'http://yui.yahooapis.com/2.2.2/build/animation/animation-min.js'
], loadComplete);

If necessary, you can also have LazyLoad pass an argument to the callback function or execute the callback in a specific scope:

// Pass an argument to the callback function.
LazyLoad.load('http://example.com/foo.js', loadComplete, 'custom argument');

// Execute the callback function in the specified scope.
LazyLoad.load('http://example.com/bar.js', loadComplete, this, true);

For more usage details, see the source documentation or the unminified source code.

If you'd like to use LazyLoad, you can either include it directly from my server as demonstrated above, or you can download it and host it yourself:

LazyLoad works in Firefox 2.0, IE6, IE7, Opera 9.2, and Safari 2.0 and 3.0. If you run into any problems, please let me know.

Comments

Nice! However, it looks like the majority of the load function exists to support callbacks. I would suggest providing an optimized path for LazyLoad.load requests that don't include a callback.

Also, perhaps you should make the pending and loaded members objects in the form of { script_url : (true|false) } to allow for parallel processing from separate callers or iterative calls with separate callbacks. Example


LazyLoad.loadOnce('foo.js',function () { /* do sum'n */ });
LazyLoad.loadOnce('bar.js',function () { /* do sum'n else */ });
Wednesday May 30, 2007 @ 07:43 AM (PDT) Posted by Luke

Technically it is possible to call load() or loadOnce() without providing a callback (although, as you say, it could be optimized), but I'm not sure I want to encourage that style of usage. If you ever find that you're calling LazyLoad without providing a callback, that's a good indication that you either don't actually need LazyLoad or that you're living dangerously and will soon be bitten by race condition bugs.

I also intentionally implemented LazyLoad in such a way as to make parallel calls impossible, since that would add all kinds of complexity for very little benefit. As it is, you can safely make several calls to load() or loadOnce() and be assured that your callbacks will be executed in the order that they were provided. Parallel loading would make that order unpredictable and would put a greater burden on developers to check for race conditions.

Wednesday May 30, 2007 @ 09:56 AM (PDT) Posted by wonko
I haven't looked in the code of this or anything, but I don't understand how this script is suppose to work.

Are you loading (downloading) the script when an event is fired, or are you loading (browser execution) the script when an event is fired? And wouldn't either one slow down the dynamic of whatever it is that is suppose to happen?
Wednesday May 30, 2007 @ 10:02 AM (PDT) Posted by Sean

When you load a script with LazyLoad, the browser downloads and executes it.

Whether or not this results in a user-visible slowdown depends on when you do it. You definitely wouldn't want to wait to download a huge library until the exact moment you need it, but if you're creative, you can find ways to start loading the library a short time before you'll need it, such as when the user moves the mouse near a certain area of the page or carries out certain actions that will lead to a need for the library.

The idea is to move the delay out of the initial pageload (where it's very noticeable) and instead load things at a time when the delay is less noticeable or less of an inconvenience.

Wednesday May 30, 2007 @ 10:20 AM (PDT) Posted by wonko
Actually, it doesn't guarantee execution order. It's using setTimeout if there's a pending download. If more than two calls are made in quick succession, there is equal chance that any of the subsequent calls could be the next executed. To guarantee order, you'd need to queue the calls internally and shift after the first completes (prob after the callback execution).

That said, I think it would be silly to have a page peppered w/ LazyLoad calls.

I'm not sure if there are xbrowser (or moral) issues with this method, but you could probably avoid the setInterval by moving its anon function to a named inner function (say loadComplete) then


if (navigator.userAgent.match(/msie/gi) && !this.webkit &&
!navigator.userAgent.match(/opera/gi)) {
script.onreadystatechange = function () {
if (this.readyState === 'loaded') {
loadComplete();
}
};
} else {
script = document.createElement('script');
script.onerror = loadComplete;
script.src = 'javascript: void(0);';

document.body.appendChild(script);
}


Full disclosure: I didn't test the method beyond POC :)
Wednesday May 30, 2007 @ 03:18 PM (PDT) Posted by Luke

You're right, there is a potential for a setTimeout race condition. I'll implement a queue to fix that as suggested.

As for the second suggestion, I do have moral issues with abusing onerror like that, but there might be another way of achieving that behavior and avoiding the setInterval. I'll give it some thought.

Wednesday May 30, 2007 @ 05:28 PM (PDT) Posted by wonko

I've released LazyLoad 1.0.1, which implements these suggestions. Thanks!

Wednesday May 30, 2007 @ 07:43 PM (PDT) Posted by wonko

], lazyLoaded); Nice job, this class is really great! :)

cheers

Thursday June 28, 2007 @ 12:01 PM (PDT) Posted by Davis Zanetti Cabral

Yesterday i try to use LazyLoad. It seems good, but i saw that new script adds to document.body . Why You do that? Why you don't use HEAD element? I do some corrects in script, like that:

... document.getElementsByTagName('HEAD')[0] ... appendChild ...

So, if you really need to add new script to body, let me know why.

Wednesday July 04, 2007 @ 11:21 PM (PDT) Posted by Victor Nazarov

I have the same question as Nazarov-- why did you decide to put the script in the body instead of the head?

Tuesday July 10, 2007 @ 11:19 AM (PDT) Posted by Kalvin

Because document.body.appendChild() is easier to type and faster to execute than document.getElementsByTagName('head')[0].appendChild() and because there's no benefit to putting the script in the head.

Tuesday July 10, 2007 @ 11:31 AM (PDT) Posted by wonko

Thanks! I see. One more clarification-- as a test I'm calling a slightly modified version of LazyLoad from a bookmarklet. The only difference is at the bottom of my version I write:

var scripts = new Array('http://www.mysite.com/~kalvin/js/prototype.js', 'http://www.mysite.com/~kalvin/js/scriptaculous.js'); LazyLoad.load(scripts);

Unfortunately, Firebug shows that triggering this bookmarklet on any page results in multiple errors including "LazyLoad is not defined" and "Prototype is not defined." For the latter problem, it seems like the queue does not actually ensure that both files are loaded in order (Scriptaculous requires Prototype and can't find it.) For the LazyLoad problem, it seems like the inserted .requestcomplete() in the body is called before LazyLoad itself has loaded. I guess this is impossible since the insertion is done inside LazyLoad... any idea as to what might be going on?

Thanks so much for your help!

Tuesday July 10, 2007 @ 02:06 PM (PDT) Posted by Kalvin

Update-- it was an issue with the way Scriptaculous injects its own javascript files.

http://dev.rubyonrails.org/attachment/ticket/8722/scriptaculous-xslt-firefox.diff

fixed! Great script, thanks.

Tuesday July 10, 2007 @ 05:19 PM (PDT) Posted by Kalvin
Thanks for the library, it work great! I've found only one issue so far by using it with IE. The issue is that sometime, the readyState of a loaded script is set to 'complete' instead of 'loaded'.

To fix, change line 87 from this:

if (this.readyState === 'loaded') {

To this:

if (this.readyState === 'loaded' || this.readyState === 'complete') {

Thanks!
Thursday July 26, 2007 @ 08:42 AM (PDT) Posted by Dany

Hi, wonko! I use your lazylib as transport layer in my own JS Loading Engine. It works good :) I will create OS project for my lib and i want to post a link to LazyLoad.js, and post it on download page. I need persist link to your last build. Can you give me that?

Thanks.

Thursday August 02, 2007 @ 12:19 AM (PDT) Posted by Victor Nazarov

Hey,

If you're getting the dreaded "Operation Aborted" error in Internet Explorer, you can get around this by changing the following:

Inside of load:
if (!document.body || !document.body.firstChild) {
    window.setTimeout(function() { LazyLoad.load(urls, callback, obj, scope); }, 50);
    return;
}

This adds a listener that waits for document.body and document.body.firstChild. Once you have a body and a first child, you can change the document.body.appendChild calls to:

document.body.insertBefore(script, document.body.firstChild);

By not writing to the end of the DOM document (while it is potentially open), you can eliminate the "Operation Aborted" message.

Wednesday October 17, 2007 @ 05:43 PM (PDT) Posted by Jakob Heuser

Hi jakob,can you please explain better where I must put that code. thanks

Tuesday October 30, 2007 @ 02:17 PM (PDT) Posted by daniel

Hi daniel,

That statement can be put right at the top of the load() method in Ryan's script. All it does is make sure document.body and document.body.firstChild exist before trying to insert anything.

The second call can be found by looking for "appendChild" and changing the line as per the statement above.

We've come back to LazyLoad several times at Gaia, and are rolling it out for event based loading. When I have some examples / docs / etc I'll roll it out and link it here (or just hit me up via the contact form on my page and I'll let you know when I finish it.)

Wednesday November 14, 2007 @ 03:40 PM (PST) Posted by Jakob Heuser

I am glad to have this function. I would like to call a JS file called “sites.js” and would like it to be completely loaded and then continue… I am not sure what is the Callback… How to do the coding? Thanks. [Pls don’t laugh. You guys are too proficient]

Wednesday May 07, 2008 @ 05:14 AM (PDT) Posted by Michelle Law
Post a comment

Basic XHTML (including links) is allowed, just don't try anything fishy. Your comment will be auto-formatted unless you use your own <p> tags for formatting. You're also welcome to use Textile or Markdown.

Don't type anything here unless you're an evil robot:


And especially don't type anything here:

Copyright © 2002-2008 Ryan Grove. All rights reserved.
Powered by Thoth.