Update: A New & Improved jQuery Script to Automatically Preload images from CSS

Posted by Scott on 06/04/2008

Topics:

7/24/08 Update: Fixed support for Opera and @import CSS (thanks http://marcarea.com/).
6/21/08 Update: This script is now updated with optional settings for load status, including a percentage bar! We also included a bug fix for IE when loading absolute image paths (Thanks Sam Pohlenz!). Details are below.

When we first launched the lab, we released a jQuery plugin that automatically preloads all images referenced in CSS files. We've found the script to be incredibly helpful in developing snappy applications where images are always ready when we need them. This post describes a significant update to the script which will make it even easier to integrate in existing projects.

The concept behind this script

If you're interested in reading about how and why we developed this script, please read our original article. Keep in mind that this update provides a new version of the code which is highly recommended over the first version.

New version improvements

Among other small improvements, this release allows preloading images from any directory specified in the CSS. Also offers load status updates for text and image-based load bars.

Load images from anywhere; no arguments!

The first version of the script preloaded images from a single directory. Unfortunately, this limitation meant the script would not work well with web applications using images located in several directories or even other web sites entirely. This updated version loads images relative to their stylesheet's url, allowing them to load no matter where they are located on the web. The new code is detailed below:

The source code

The source code for preloadCssImages.jQuery_v4.js

/**
 * --------------------------------------------------------------------
 * jQuery-Plugin "preloadCssImages"
 * by Scott Jehl, scott@filamentgroup.com
 * http://www.filamentgroup.com
 * reference article: http://www.filamentgroup.com/lab/update_automatically_preload_images_from_css_with_jquery/
 * demo page: http://www.filamentgroup.com/examples/preloadImages/index_v4.php
 * 
 * Copyright (c) 2008 Filament Group, Inc
 * Dual licensed under the MIT (filamentgroup.com/examples/mit-license.txt) and GPL (filamentgroup.com/examples/gpl-license.txt) licenses.
 *
 * Version: 4.0, 07.24.2008
 * Changelog:
 * 	02.20.2008 initial Version 1.0
 *    06.04.2008 Version 2.0 : removed need for any passed arguments. Images load from any and all directories.
 *    06.21.2008 Version 3.0 : Added options for loading status. Fixed IE abs image path bug (thanks Sam Pohlenz).
 *    07.24.2008 Version 4.0 : Added support for @imported CSS (credit: http://marcarea.com/). Fixed support in Opera as well.
 * --------------------------------------------------------------------
 */
jQuery.preloadCssImages = function(settings){
	var settings = jQuery.extend({
		statusTextEl: null,
		statusBarEl: null
	}, settings);
	
	var allImgs = [];//new array for all the image urls  
	var k = 0; //iterator for adding images
	var sheets = document.styleSheets;//array of stylesheets
	
	for(var i = 0; i<sheets.length; i++){//loop through each stylesheet
		var cssPile = '';//create large string of all css rules in sheet
		var csshref = (sheets[ i ].href) ? sheets[ i ].href : 'window.location.href';
		var baseURLarr = csshref.split('/');//split href at / to make array
		baseURLarr.pop();//remove file path from baseURL array
		var baseURL = baseURLarr.join('/');//create base url for the images in this sheet (css file's dir)
		if(baseURL!="") baseURL+='/'; //tack on a / if needed
		if(sheets[ i ].cssRules){//w3 
			var thisSheetRules = sheets[ i ].cssRules; //w3 
			for(var j = 0; j<thisSheetRules.length; j++){ 
				if ( sheets[ i ].cssRules[ j ].constructor == 'CSSImportRule' ) { //added support for @imported css - credit: http://marcarea.com/
					var importSheetRules = sheets[ i ].cssRules[ j ].styleSheet.cssRules; 
					for ( var x=0; x<importSheetRules.length; x++ ) { 
						cssPile+= importSheetRules[ x ].cssText; 
					} 
				} 
				else { 
					cssPile+= thisSheetRules[ j ].cssText; 
				} 
			} 
		} 
		else { 
			if ( sheets[ i ].imports.length > 0 ) { 
				for (var m=0; m<sheets[ i ].imports.length; m++) { 
					cssPile+= sheets[ i ].imports[ m ].cssText;
				} 
			} 
			else { 
				cssPile+= sheets[ i ].cssText; 
			} 
		}
		//parse cssPile for image urls and load them into the DOM
		var imgUrls = cssPile.match(/[^\(]+\.(gif|jpg|jpeg|png)/g);//reg ex to get a string of between a "(" and a ".filename"
		var loaded = 0; //number of images loaded

		if(imgUrls != null && imgUrls.length>0 && imgUrls != ''){//loop array\
			var arr = jQuery.makeArray(imgUrls);//create array from regex obj	 
			jQuery(arr).each(function(){
				allImgs[k] = new Image(); //new img obj
				allImgs[k].src = (this.charAt(0) == '/' || this.indexOf('http://')>-1) ? this : baseURL + this;	//set src either absolute or rel to css dir
				
				if(allImgs[k].src.lastIndexOf('http://')>0){allImgs[k].src = allImgs[k].src.split('%22')[1];} //fixed opera's source doubling
				
				$(allImgs[k]).load(function(){
					loaded++;
					//send updates to status elements if applicable
					if(settings.statusTextEl) {
						$(settings.statusTextEl).html('<span class="numLoaded">'+loaded+'</span> of <span class="numTotal">'+allImgs.length+'</span> loaded (<span class="percentLoaded">'+(loaded/allImgs.length*100).toFixed(0)+'%</span>) <span class="currentImg">Now Loading: <span>'+allImgs[loaded-1].src.split('/')[allImgs[loaded-1].src.split('/').length-1]+'</span></span>');
					}
					if(settings.statusBarEl) {
						var barWidth = $(settings.statusBarEl).width();
						$(settings.statusBarEl).css('background-position', -(barWidth-(barWidth*loaded/allImgs.length).toFixed(0))+'px 50%');
					}
				});
				k++;
			});
		}
	}//loop
	return allImgs;
}				

How do I use it?

Paste the code above into your JavaScript file and run $.preloadCssImages(); when the DOM is ready. Don't forget that you'll also need to reference the jQuery javascript library in your page.

Additional options

You can optionally pass element selectors to the plugin which will receive updates on the load status. These settings are specified as statusTextEl and statusBarEl. The values must be jQuery selector strings, for example: $.preloadCssImages({statusTextEl: '#textStatus', statusBarEl: '#status'});. To make a preload bar, cut a background image to the same width as your status bar element and assign it to the background of your element in CSS. Be sure to set its background-repeat to 'no-repeat'. The script will handle the positioning.

A quick demo

The example below uses our script to parse through a sample stylesheet which we've linked to the page. The sample stylesheet has background images specified for elements that don't actually exist on the page, so the images specified are not currently loaded. Clicking the button below will load them into the DOM. For example purposes, we'll write the loaded images into the page as well. The optional load status elements are shown upon load.

Download preloadCssImages.jQuery_v4.js

This script is a jQuery plugin, meaning is is dependant on the incredible jQuery javascript library. If you feel particularly adventurous, this script could be easily ported to another library or written in plain old JavaScript as well. Feel free to grab the script and try it for yourself. We're always looking for ways to improve our scripts, so if you encounter any issues or have any questions or suggestions please leave a comment below.

Comments

Hi I dug your script and wanted to port it to Prototype.
So here it is: http://pastie.org/209498

Comment by JDD on 06/05  at  12:12 PM

@JDD: Nice work. You’ve given us some ideas where we can optimize ours as well! Thanks.

Comment by Scott (Filament) on 06/05  at  05:47 PM

This script appears to have a problem in IE, relating to the use of square brackets to get at a character index.

To fix, line 46 should be:
allImgs[k].src = (this.charAt(0) == ‘/’ || this.match(’http://’)) ? this : baseURL + this; //set src either absolute or rel to css dir

Otherwise, great plugin. Thanks!

Comment by Sam Pohlenz on 06/20  at  03:46 PM

@Sam: Thanks for commenting. What version of IE are you seeing a problem in? We just tested the demo page in 6 and 7 and see no problems.

Comment by Scott (Filament) on 06/20  at  03:58 PM

Scott, the demo page doesn’t trigger the issue as none of the URLs in the css begin with a forward slash.

Under IE 6 & 7 (at least on my machine), “mystring"[0] gives undefined, whereas “mystring”.charAt(0) gives the desired result of ‘m’.

The result is that presently under IE, any absolute css urls beginning with / will be fetched as if they were relative urls.

Comment by Sam Pohlenz on 06/20  at  04:24 PM

Thanks Sam. It’s an interesting case that we’ll have to look into. I tried pasting in your charAt change real quick and it caused errors for me in FF and Safari. Not sure what would cause those but we’ll have to take a look at this next week and see. Thanks for following up though, we’ll let you know when we figure out a fix for your case.

Comment by Scott (Filament) on 06/20  at  05:43 PM

Great work, this will really save a lot of time. Just concerned about IE issues though.

Comment by Abhimanyu Grover on 06/21  at  08:49 AM

6/21/08 Update: This script is now updated with optional settings for load status, including a percentage bar! We also included a bug fix for IE when loading absolute image paths (Thanks Sam Pohlenz!).

Comment by Scott (Filament) on 06/21  at  11:22 AM

Just a few suggestions :

Check the media type of the stylesheet :

var mediaText = typeof sheets[ i ].media.mediaText !== ‘undefined’ ? sheets[ i ].media.mediaText : sheets[ i ].media;
if ( mediaText.indexOf(’screen’) ) {
continue;
}

And check @import rules for csshref:

if ( csshref == null ) {// consider @import rules
for ( var z=0; z<sheets[ i ].cssRules.length; z++) {
if ( sheets[ i ].cssRules[z].constructor == CSSImportRule ) {
csshref = sheets[ i ].cssRules[z].href;
}
}
}

Consider @import rules, somebody may write it better smile

if(sheets[ i ].cssRules){//w3
var thisSheetRules = sheets[ i ].cssRules; //w3
for(var j = 0; j<thisSheetRules.length; j++){
if ( sheets[ i ].cssRules[j].constructor == CSSImportRule ) {
var importSheetRules = sheets[ i ].cssRules[j].styleSheet.cssRules;
for ( var x=0; x<importSheetRules.length; x++ ) {
cssPile+= importSheetRules[x].cssText;
}
} else {
cssPile+= thisSheetRules[j].cssText;
}
}
}
else {
if ( sheets[ i ].imports.length > 0 ) {
for (var m=0; m<sheets[ i ].imports.length; m++) {
cssPile+= sheets[ i ].imports[m].cssText
}
} else {
cssPile+= sheets[ i ].cssText;
}
}

Comment by Marc on 06/21  at  11:40 AM

@Marc: Great catch, thanks so much. We’ll add it in there ASAP!

Comment by Scott (Filament) on 06/21  at  11:50 AM

Have you tested it with Opera? I’m using Opera 9.5 on XP SP3 and it doesn’t work. The image URLs are like this:
http://www.filamentgroup.com/examples/preloadImages/&#x22http://www.filamentgroup.com/examples/preloadImages/images/bg_header.jpg
instead of this:
http://www.filamentgroup.com/examples/preloadImages/images/bg_header.jpg
(which is what I get with Fx, IE and Safari)

Comment by Antonio Bueno on 06/23  at  05:35 AM

@Antonio: Thanks, we’ll look into it. Looks like a regression in the last update.

Comment by Scott (Filament) on 06/23  at  08:34 AM

and how do i use this with body onload?? i dont want users to have to click

Comment by Michal on 08/07  at  04:58 AM

@Michal: You can refer to the section above “How do I use it?” for usage instructions, but basically, it’s a function that you can call whenever you’d like. The demo page runs on a click event because it’s easier to see what’s taking place that way.

To run it on dom ready:
$(function(){
$.preloadCssImages();
});

To run on body load:
$(’body’).load(function(){
$.preloadCssImages();
});

Comment by Scott (Filament) on 08/07  at  08:29 AM

Hi Scott,

very nice script. i needed something like this, but i had some problems with multiple css-files in different folders. so i fixed some bugs and improved style-parsing performance.

here you can find the script with a nice demo:
http://www.protofunc.com/scripts/jquery/cssImagePreload/

Comment by alexander on 08/09  at  01:22 PM

Receive “access is denied” on line 58.
cssPile+= sheets[ i ].cssText;

Could this be cause by a cross-domain issue?

Comment by Rick on 08/20  at  01:34 PM

Hi there, lovely plugin, only thing is, it doesnt seem to work with using fadein, I can see in statustext that its loaded, but on mouseover event, I can see firefox3 still loading the image - causing a flicker with rollover effect.

Any suggestions to why this is happening ?

Kind regards

zenmaster

Comment by Zenmaster on 08/24  at  04:41 AM

Hi there- I’m excited to try this out. Would you be able to say more specifically where you put the call to the script? In the head, the body, the bottom? I know you say “when the dom is ready” but I’m still getting my head around all this DOM stuff. Thanks!

Comment by Terrence on 09/04  at  01:41 PM

@Terrence: jQuery has a custom event that allows you to run code as soon as the DOM is ready (the elements are there but images aren’t yet loaded). You can use it like this:

$(document).ready(function(){
//put your DOM-related scripting here
});

Comment by Scott (Filament) on 09/04  at  01:48 PM

Hi,

Firebug gives the following error:

Security error” code: “1000
if(document.styleSheets.cssRules){//w3

I get this error when I access my website using http://www.raihaniqbal.net which basically points to http://www.raihaniqbal.org. But it works when I use the latter url.

Any idea?

Comment by Raihan on 09/17  at  12:46 AM

There’s a bug in your script, on or about line 40:

if ( sheets.cssRules[j].constructor == ‘CSSImportRule’ ) { //added support for @imported css - credit: http://marcarea.com/

should read instead:

if ( sheets.cssRules[j].constructor == CSSImportRule ) { //added support for @imported css - credit: http://marcarea.com/

Note the absence of quotes around CSSImportRule. The equivalence operation for the constructor needs to be made against an object, not a string.

Comment by Daniel Wright on 09/17  at  01:55 PM

@Daniel
This and some other bugs are fixed in this version:
http://www.protofunc.com/scripts/jquery/cssImagePreload/

I hope there will be an new bugfix and CPU- + http-performance release by Scott.

Comment by alex on 09/18  at  05:47 PM

Add a Comment:* required fields