Search for Programming, AngularJS, Rails, Testing ...

Cross Browser Stylesheet Preloading

Preload stylesheets with ease

Since this website focus is primarily on JavaScript and AJAX, I figured it would be worth while to discuss the mechanics behind cross-browser stylesheet preloading because this topic hasn't been effectively solved and discussed about online. Preloading (downloading in the background) stylesheets is tricky, but its a solid requirement for creating advanced AJAX-based websites. So how do we get this to work? What's the problem?

To preload a stylesheet file, we simply create a <link> tag with the attributes rel="stylesheet", type="text/css" and href="/path/to/stylesheet" and set an onload property right? Well, popular browsers such as Chrome, Firefox and Safari do not support the onload event/property for link tags, so natively there is no method of getting the onload event to fire when the stylesheet has been loaded.

Last Updated

This blog entry was last updated on March 23rd 2011 and was created on March 21st 2011.

* An Update for Firefox 4

Firefox 4 was just recently released and it too does not support the onload event for <link> elements.

* An Update for IE 9

IE 9 was just recently released and it does support the onload event for <link> elements (just like older versions of IE).

OK, so tell me about the onload event

Only Internet Explorer browsers (IE6 ... IE9) and Opera support the onload property for link tag elements. The onload event will fire once the stylesheet has been downloaded and added to the DOM. Now the onload property is supported for other elements, such as Images, Script Tags, and the Body tag, however for some reason its been left out of the feature set for Firefox, Chrome and Safari.

Basically, what we want to get is this:

new RemoteStylesheet('/path/to/stylesheet.css',{

  onLoad : function(link,href) {
    alert('stylesheet loaded!');
  },

  onError : function(link) {
    alert('something messed up :(');
  }
});
Feature IE6, IE7, IE8 & IE9 Firefox Chrome Safari Opera
onload event Supported Not Supported Not Supported Not Supported Supported
onload (via HTML) event Supported Not Supported Not Supported Not Supported Supported
onerror event Supported Not Supported Not Supported Not Supported Supported
onerror (via HTML) event Supported Not Supported Not Supported Not Supported Supported

so what are the options to get this to work?

we have eight options:

  1. setup a global onload event for the html page.
  2. setup a blank iframe with an document.onload property.
  3. create the link element and setup a timer to examine the the cssrules
  4. use the xhr wrapper to download the stylesheet file and read the contents
  5. use the xhr wrapper to download to download the file and assume that its been cached
  6. use the xhr wrapper with a local proxy to download the contents
  7. setup some sort of flag within the stylesheet file
  8. something a little more custom

1. Using a Global document.body.onload event

Does this solve the problem? NO

The <body onload="fn()"> will fire once all of the external images, scripts, frames and stylesheets are downloaded. Its useful as a final check once a page is ready, but with DOMContentLoaded event used (emulated as onDomReady in MooTools and document.ready() in JQuery) there's really no need to use it. If it were to be used for preloading stylesheet files then it could really only be used when the page is downloaded and accessed as a traditional HTML file download. So for pure AJAX websites (like this one) it won't really do ... and also the body.onload event won't fire again after it has fired once. Furthermore, all the other crap such as frames, images, scripts and so on will be also considered for the event when its fired (they must all be downloaded as well). Finally if only stylesheets were to be used then it would not be possible to track the progress of one stylesheet at a time. Therefore, document.body.onload is out of the question.

So does this solve the problem? NO!

2. Setup a blank IFrame with a iframe.contentWindow.document.body.onload property

Does this solve the problem? NO

The next option would be to create a blank IFrame every time a stylesheet is to be preloaded. This option is pretty clever, but its very limiting. First we create a blank IFrame (without a src attribute) and set the HTML contents directly. Then we insert the HTML to create the <link> tag with the correct href attribute pointing to the stylesheet. Once that's ready, code the <body onload="..."> and point the event to point to a variable (or the top.window) to trigger a onload function and set the parameters to include the particular stylesheet URL or ID that's being downloaded. Once the IFrame is fully loaded (the onload event fires) we know when the stylesheet file has been downloaded (since that's the only thing there).

This would work (and work well) if there wasn't the issue with the fact that the stylesheet is within the environment (page) of the IFrame. Thus we cannot transfer the stylesheet outside of the IFrame to the main webpage (the page where the JavaScript is being run from) since most browsers will throw some sort of security exception since you're trying to change the parent node of the stylesheet to that of being a node that's in another document.

OK, maybe if the stylesheet has been downloaded within the IFrame then it would be apart of the page cache and therefore if it gets included a standard document.createElement('link') operation within the main document then it would be loaded instantly from the cache right? Well maybe, but this isn't a solid solution. Given the Request/Response HTTP headers associated with the GET request, the file may or may not be cached at all and this relies of if the browser used handles its asset files like so.

So does this solve the problem? NO!

3. Create the link element and setup a timer to examine the the cssRules

Does this solve the problem? NO

Now we're getting somewhere, but still no cigar. What we can do is setup a script to create a new <link> element and apply the type="text/css", rel="stylesheet" and href="/path/to/stylesheet.css" file. Once we append that to the DOM, we can setup a timer (setTimeout) operation which periodically checks to see what's going on with the link element. So at each timeout operation, you check to see if you can find the link element in the DOM, then you check to see if you can access its contents. If you can access the contents, then the file has been downloaded and has therefore been fully preloaded.

This raises the following questions and concerns. Does accessing the contents mean that the file has been fully downloaded? And what about stylesheet files outside of the origin (cross-site)?

So does this solve the problem? NO!

Examine the contents

OK so the contents of the stylesheet file can be read and examined. This doesn't necessarily mean that the file has been downloaded. There is no final event that fires once all the contents have been parsed. Not good enoughh...

Stylesheet files outside the origin

Keep in mind that as a security precaution, browsers like Chrome, Safari and Firefox do not allow reading the contents of the stylesheet link element if it belongs to a domain that is not of the same origin as the current web page. So how do you read the contents? You can't. Too bad. Screw this!

So does this solve the problem? NO!

4. Use the XHR wrapper to download the stylesheet file and read the contents

Does this solve the problem? NO

Every mediocre web developer is aware of the XHR cross-site request limitation. There are some hacks (like XDomainRequest and Flash), but its really just not possible. So the only way you can read stylesheet contents is if they are within the same origin (same domain) as the webpage. Therefore, much like the previous method, you will be limited exclusively to preloading stylesheets that are of the same origin. Better luck next time.

So does this solve the problem? NO!

5. Use XHR, JSON, Images, IFrames or any other cross-domain method

Does this solve the problem? NO

Almost as big of a failure as the 4th method (infact its an amalgamation of method 4 and method 2). If you somehow manage to get to request the Stylesheet file with some method of cross-site ajax, script tag download (think JSONP), or image src and assume that its within the browsers cache then you're wrong. Much like the concerns addressed in method 2 (HTTP Headers and so on) it will not guarantee that the file has been downloaded and cached. NEXT!

So does this solve the problem? NO!

6. Use the XHR wrapper with a local proxy to download the contents

Does this solve the problem? Kinda

Getting warmer... This solution almost does the trick but it comes with a penalty. A server-side script is required so that it proxies (buffers) the contents of the remote URL. Once the XHR request is sent, the returned contents will be that of the CSS stylesheet, but will be redirected via the server-side proxy'ng script thus following the same-origin policy.

OK so the contents have been fetched ... what now? Well, its just a matter of creating an empty stylesheet and applying the rules. There is no guarantee that all of this will work, but it should do the trick. The problem however is that the operation itself relies on the proxy and in turn puts all extra work that the browser should be doing 100% on the website server. So really is this an ideal solution? I don't think so...

So does this solve the problem? Kinda

7. Setup some sort of flag within the stylesheet file

Does this solve the problem? Almost

This solution is pretty good, but it requires extra work on both the stylesheet and the website page. Why? Because what you need todo is essentially setup a flag (some property or class declaration) that will make an impression on the page in a way that the JavaScript can figure out that it has been applied. This can work and you can use really esoteric css properties that wouldn't really make any effect on the page (think clip properties, but used for statically positioned elements which equals no effect) and then have the javascript code figure out if the change had occurred. There might be browser inconsistencies, and the values may not even update properly (CSS specificity clashes) or at least right away and other scripts may interfere with the "esoteric" style changing.

So does this solve the problem? Almost

8. Something a little more custom

Does this solve the problem? YES!

Finally! A solution! Yes this does the trick and it works in all browsers. Its a simple solution that creates a link element, a timed interval, the onload property for IE and Opera, and DOM checking. So how does this work? Lets find out.

var RemoteStyleSheet = new Class({

  Implements : [Options,Events],

  options : {
    delay : 100,
    maxAttempts : 1000,
    idPrefix : 'css-preload-'
  },

  initialize : function(path,options) {
    this.setOptions(options);
    this.path = path;
    this.id = this.options.idPrefix + (new Date().getTime());
  },

  getPath : function() {
    return this.path;
  },

  getID : function() {
    return this.id;
  },

  getElement : function() {
    return this.link;
  },

  _onready : function() {
    var args = [this.getPath(),this.getElement()];
    this.fireEvent('ready',args);
  },

  _onerror : function() {
    this.link.destroy();
    this.link = null;
    var args = [this.getPath()];
    this.fireEvent('error',args);
  },

  _onstart : function() {
    var args = [this.getPath(),this.getElement()];
    this.fireEvent('start',args);
  },

  createLinkElement : function() {
    this.link = document.createElement('link');
    this.link.type = 'text/css';
    this.link.rel = 'stylesheet';
    this.link.id = this.getID();
    this.link.href = this.getPath();
    return this.link;
  },

  _checker : function() {
    if(!this.counter) {
      this.counter = 0;
    }

    var target = $(this.getID());
    if(target.sheet) {
      var stylesheets = document.styleSheets;
      for(var i=0;i&lt;stylesheets.length;i++) {
        var file = stylesheets[i];
        var owner = file.ownerNode ? file.ownerNode : file.owningElement;
        if(owner && owner.id == this.getID()) {
          this._onready();
          return;
        }

        if(this.counter++>this.options.maxAttempts) {
          this._onerror();
          return;
        }
      }
    }

    this._checker.delay(this.options.delay,this);
  },

  start : function() {

    var fn;
    this.link = this.createLinkElement();
    if(Browser.ie || Browser.opera) {
      this.link.onload = this._onready.bind(this);
      this.link.onerror = this._onerror.bind(this);
    } 
    else {
      fn = this._checker.bind(this);
    }

    document.getElement('head').appendChild(this.link);
    this._onstart();

    if(fn) {
      fn();
    }
  }

});

Here is what happens:

  1. 1. The Class is instantiated and the options are set.
  2. 2. Once the start() method is called then it creates a link element with the type, rel, href and a UNIQUE ID
  3. 3. Then, if the browser is IE or Opera, then the onload method is set and for all other browsers a timed interval is started
  4. 4. During the interval all the stylesheets in the dom (document.styleSheets) are compared to that of the current link element by comparing the ownerNode.id to the ID that was created within the class. If there is a match then the stylesheet has been loaded properly (it is only when the stylesheet has been downloaded properly will it then be placed in the DOM).
  5. 5. Once there is a match, then the onLoad event is fired

And here is how to use the class:

new RemoteStyleSheet('/path/to/stylesheet.css',{
    
  onReady : function() {
    alert('we are ready');
  },

  onError : function() {
    alert('damn too bad');
  },

  onStart : function() {
    alert('starting to download');
  }

}).start();

So does this solve the problem? YES!

Demos

This wouldn't be complete without demos.

Simple Background CSS File Change

Click here to view this demo

CSS Stylesheet Control Panel

Click here to view this demo

Issues

There is only one issue for the browsers that use the timed interval solution. The issue is that if the onerror property does not fire when the stylesheet file is not found at all. If however, the maxAttempts counter runs out before the onload event fires then the onerror event will fire, however that is unlikely since with a delay of 100 and a maxAttempts total of 1000 that equals 100,000 milliseconds which is 100 seconds.

Feedback & Contact

If something isn't clear or you're very happy to learn this technique then feel free to contact me at the email (Currently my email contact form has not been developed yet so just email me at: feedback [at] [this domain without extension or the www] [dot] com).