Painless JavaScript lazy loading with LazyLoad

Tuesday May 29, 2007 @ 11:09 PM (PDT)

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://cdn.wonko.com/lazyload/1.0.4/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.5.2/build/yahoo-dom-event/yahoo-dom-event.js',
  'http://yui.yahooapis.com/2.5.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.x, Firefox 3.x, IE6, IE7, Safari 3.x (including iPhone) and Opera 9.x. 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 */ });
Gravatar icon
Wednesday May 30, 2007 @ 07:43 AM (PDT)

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.

Gravatar icon
Wednesday May 30, 2007 @ 09:56 AM (PDT)
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?
Gravatar icon
Wednesday May 30, 2007 @ 10:02 AM (PDT)

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.

Gravatar icon
Wednesday May 30, 2007 @ 10:20 AM (PDT)
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 :)
Gravatar icon
Wednesday May 30, 2007 @ 03:18 PM (PDT)

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.

Gravatar icon
Wednesday May 30, 2007 @ 05:28 PM (PDT)

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

Gravatar icon
Wednesday May 30, 2007 @ 07:43 PM (PDT)

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

cheers

Gravatar icon
Thursday June 28, 2007 @ 12:01 PM (PDT)

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.

Gravatar icon
Wednesday July 04, 2007 @ 11:21 PM (PDT)

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

Gravatar icon
Kalvin
Tuesday July 10, 2007 @ 11:19 AM (PDT)

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.

Gravatar icon
Tuesday July 10, 2007 @ 11:31 AM (PDT)

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!

Gravatar icon
Kalvin
Tuesday July 10, 2007 @ 02:06 PM (PDT)

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.

Gravatar icon
Kalvin
Tuesday July 10, 2007 @ 05:19 PM (PDT)
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!
Gravatar icon
Dany
Thursday July 26, 2007 @ 08:42 AM (PDT)

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.

Gravatar icon
Thursday August 02, 2007 @ 12:19 AM (PDT)

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.

Gravatar icon
Wednesday October 17, 2007 @ 05:43 PM (PDT)

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

Gravatar icon
Tuesday October 30, 2007 @ 02:17 PM (PDT)

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.)

Gravatar icon
Wednesday November 14, 2007 @ 03:40 PM (PST)

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]

Gravatar icon
Michelle Law
Wednesday May 07, 2008 @ 05:14 AM (PDT)

Hey Daniel, the script works GREAT for loading once the page has.

Quick question, I’m trying to load banner ad’s and the ad’s aren’t showing up in the position they’re called from. Have you run into this before and is there a solution for this?

Gravatar icon
Friday January 16, 2009 @ 03:36 PM (PST)

Sorry, previous comment meant for Ryan.

Gravatar icon
Friday January 16, 2009 @ 03:37 PM (PST)

Do the ads use document.write? That won’t play well with LazyLoad, since the loading occurs asynchronously (potentially after the rest of the page has finished loading) and the write doesn’t occur until the script actually executes.

Gravatar icon
Friday January 16, 2009 @ 04:14 PM (PST)

If you are having problems with document.write with LazyLoad, you can experiment with overriding the default document.write method. For example, I have used this in some POC code:

document.write = function(html) {
	with (arguments.callee) {
		var element = (typeof _documentWriteElement == "undefined") ? "body" : _documentWriteElement;
	}
	jQuery(element).append(html);
};

_documentElement would then be defined in the global scope and can be redefined in closures to override that default.

Gravatar icon
J5
Tuesday March 10, 2009 @ 01:05 PM (PDT)

nice work! thanks!

Gravatar icon
lil bit
Friday May 15, 2009 @ 05:00 AM (PDT)

Great script, thanks! Now my site is no longer a victim of slow external js-based widgets such as Woopra, UserVoice, etc.

Saved my day!

Gravatar icon
Jossi
Tuesday June 02, 2009 @ 10:55 AM (PDT)

I have a callback function that adds an inline style and an anchor tag to the page. In MSIE, this has the unfortunate effect of only that inline style and a tag being loaded in the browsers.
What happens is this:
Page loads OK, then as soon as the lazyload for the script is completing loading, the page gets replaced with that inline style and anchor tag, everything else disappears.

Any ideas?

Gravatar icon
Jossi
Tuesday June 02, 2009 @ 03:14 PM (PDT)

Or will I be required to add LazyLoad.loadOnce to the _spBodyOnLoadFunctionNames.push array?

Gravatar icon
panoone
Monday October 26, 2009 @ 05:25 PM (PDT)

For some reason, when I try to load Scriptaculous the page goes blank and appears to be trying to load something but never returns. I have the fix recommended by Kalvin but it didn’t make any difference. Anyone else have the same or similar problem?

Gravatar icon
Barry
Friday January 15, 2010 @ 04:54 PM (PST)

I love LazyLoad but it’s preventing images within my page from printing.

If I include:

$(function() { $("img").lazyload({placeholder : "_images/gray.gif", effect: "fadeIn"}); });

then images never get printed.

Is there any method to disable LazyLoad during printing — something like:

onClick=“javascript:parent.print(); disableLazyLoad();”

?

Otherwise, it’s great! Thanks!

Gravatar icon
Mark P.
Monday February 22, 2010 @ 11:13 AM (PST)

Mark, the jQuery image lazyloader plugin (which appears to be what you’re having trouble with) is not related to my LazyLoad library.

Gravatar icon
Monday February 22, 2010 @ 12:56 PM (PST)

Sorry mate! Wrong LazyLoad.

=)

Gravatar icon
Mark P.
Monday February 22, 2010 @ 02:19 PM (PST)
New comment

required, won't be displayed

optional

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


And especially don't type anything here:

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.

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