When is a stylesheet really loaded?

March 17th, 2011. Tagged: browsers, CSS, performance

Often we want to load a CSS file on-demand by inserting a link node. And we want to know when the file finished loading in order to call a callback function for example.

Long story short: turns out this is harder than it should be and really unnecessary hard in Firefox. I hereby beg on behalf of many frustrated developers: please, Firefox 4, please fire a load event when a stylesheet loads.

Update:
If you think this is "a change we can believe in", tell the browser vendors to fire a load event on link elements as the HTML5 standard mandates. Here's the list of bugs for each browser to comment on and point people to:

Here's what the standard says:

"Once the attempts to obtain the resource and its critical subresources are complete, the user agent must, if the loads were successful, queue a task to fire a simple event named load at the link element"

With that out of the way, let's see what we have here.

// my callback function
// which relies on CSS being loaded
function CSSDone() {
  alert('zOMG, CSS is done');
}
  
 
// load me some stylesheet
var url = "http://tools.w3clubs.com/pagr/1.sleep-1.css",
    head = document.getElementsByTagName('head')[0];
    link = document.createElement('link');
 
link.type = "text/css";
link.rel = "stylesheet"
link.href = url;
 
// MAGIC
// call CSSDone() when CSS arrives
 
head.appendChild(link);

Options for the magic part, sorted from nice-and-easy to ridiculous

  1. listen to link.onload
  2. listen to link.addEventListener('load')
  3. listen to link.onreadystatechange
  4. setTimeout and check for changes in document.styleSheets
  5. setTimeout and check for changes in the styling of a specific element you create but style with the new CSS

5th option is too crazy and assumes you have control over the content of the CSS, so forget it. Plus it checks for current styles in a timeout meaning it will flush the reflow queue and can be potentially slow. The slower the CSS to arrive, the more reflows. So, really, forget it.

So how about implementing the magic?

  // MAGIC
  // #1
  link.onload = function () {
    CSSDone('onload listener');
  }
  // #2
  if (link.addEventListener) {
    link.addEventListener('load', function() {
      CSSDone("DOM's load event");
    }, false);
  }
  // #3
  link.onreadystatechange = function() {
    var state = link.readyState;
    if (state === 'loaded' || state === 'complete') {
      link.onreadystatechange = null;
      CSSDone("onreadystatechange");
    }
  };
  
  // #4
  var cssnum = document.styleSheets.length;
  var ti = setInterval(function() {
    if (document.styleSheets.length > cssnum) {
      // needs more work when you load a bunch of CSS files quickly
      // e.g. loop from cssnum to the new length, looking
      // for the document.styleSheets[n].href === url
      // ...
      
      // FF changes the length prematurely :( )
      CSSDone('listening to styleSheets.length change');
      clearInterval(ti);
      
    }
  }, 10);
  
  // MAGIC ends

Test

Test page - riiiight here. I'm loading a CSS file delayed two seconds on the server. Attaching all those event listeners and timeouts above. Adding another timeout that just says "... and two seconds later ... " after (you guessed it!) two seconds. Now observing los resultados...

Results

  • IE fires readystatechange and load (tested years ago, too lazy to test now again). Now with IE9 maybe addEventListener will work too?
  • Firefox (like before) fires nothing. It updates the length of document.styleSheets immediately, not waiting for the file to actually arrive. So the outcome in my test log is:
    zOMG, CSS #1 is done: listening to styleSheets.length change
    ... and two seconds later ...
    
  • Opera fires load via onload and via addEventListener too. Like FF it also increments document.styleSheets.length immédiatement. The outcome:
    zOMG, CSS #1 is done: listening to styleSheets.length change
    ... and two seconds later ...
    zOMG, CSS #1 is done: onload listener
    zOMG, CSS #1 is done: DOM's load event
  • Chrome and Safari will not fire events but will update document.styleSheets only when the file arrives, yey!
    ... and two seconds later ...
    zOMG, CSS #1 is done: listening to styleSheets.length change

All in all, there's at least one way to tell when the stylesheet is loaded in each browser, except Firefox. Now that's embarrassing.

Is there really no hope for Firefox?

If you go really crazy than yes - implement magic #5 but it has serious drawbacks.

Otherwise the object trick should do - all browsers seem to fire load and/or readystatechange event consistently. Obviously it's a little more complicated. Although probably not as complicated as monitoring the document.styleSheets collection

I also tried MozAfterPaint - it might work but didn't for me, because my CSS didn't change anything on the page that required repaint. Obviously not a fit-all solution.

Another thing that failed was checking document.styleSheets[n].cssRules. Although document.styleSheets.length is updated immediately, I was thinking FF cannot update document.styleSheets[n].cssRules (document.styleSheets[n].sheet.cssRules) until the CSS actually arrives. However since FF 3.5 (or thereabouts) you don't have access to cssRules collection when the file is hosted on a different domain (CDN say). Security thing, you see. Even cssRules.length would've been enough, but nah.

Takeaways

  1. Any ideas about a clever (or not so clever) workaround that lets us figure out when CSS is loaded in FF? Please comment.
  2. Libraries can benefit from document.styleSheets.length trick to support Chrome and Safari. I know at least that YUI3 doesn't support callbacks on Y.Get.css() in Safari (nor FF)
  3. Firefox 4 must implement load on stylesheets :) No doubt about it. IMO all browsers should fire load on everything related to external resources.

UPDATE: FF solved!

Thanks to Ryan's comment below, Zach and Oleg, turns out there is something that works. And that is:

  1. you create a style element, not a link
  2. add @import "URL"
  3. poll for access to that style node's cssRules collection

It just happens so that Firefox will not populate this collection until the file arrives!

var style = document.createElement('style');
style.textContent = '@import "' + url + '"';
 
var fi = setInterval(function() {
  try {
    style.sheet.cssRules; // <--- MAGIC: only populated when file is loaded
    CSSDone('listening to @import-ed cssRules');
    clearInterval(fi);
  } catch (e){}
}, 10);  
 
head.appendChild(style);

Updated my test and seems to work just fine.

Whew!

I still maintain that this is unnecessarily complex and all browsers should simply fire load event. Seems to me this FF behavior might change at any time, as well as Safari's not populating document.styleSheet

One thing to note: this access to cssRules is not a failure of the same security check that doesn't allow cssRules access with outside domains. In this case we're accessing cssRules of the inline style and that's fine. We still cant access the styles of the @import-ed file from a different domain.

Side note: makes me wonder - this could be a technique to preload JS without executing too :)

Tell your friends about this post: Facebook, Twitter, Google+

48 Responses

  1. Would it much simpler by:
    1) always having a “beacon” css rule in loaded CSS;
    2) have this rule to apply to a hidden “beacon” element in page?
    3) setInterval to check whether the “beacon” css rule has applied to be hidden “beacon”.

  2. yes, that’s my magic #5 :) it has serious drawbacks though (as described above)

  3. Zach Leatherman and Oleg Slobodskoi both independently found a reliable way of detecting CSS load completion in Firefox. Zach wrote about the technique on his blog, and I recently implemented it in LazyLoad. It works well.

  4. Yah, I always was frustrated with the absence of “load” for s some time ago. Think, the best approach for “dynamic css” will be to embed the css into JS (in some static pre-processsing step) – and then embed the CSS after JS load.

  5. For firefox

    create csslink -> loading xxx.css -> change some iframe style(change height or width) -> listen iframe resize event

    when resize event be fired, the css file loaded.

  6. Thankyou, thankyou, thankyou Ryan :)
    The test updated: http://www.phpied.com/files/cssonload/test.html

    Updating the post now…

    But still, FF should fire a load event. This behavior might break at any time,

  7. Here is the link for the Firefox bug on the onload issue:
    https://bugzilla.mozilla.org/show_bug.cgi?id=185236

  8. Thanks James, I couldn’t find this one, although I found one that said that this one exists :)

    Maybe the best strategy should be:
    a/ add to HTML5 standard
    b/ hold browsers accountable

    Better than “I might do this at some point” (comment #26)

  9. Thanks for the detailed write-up, Stoyan!

    Did you investigate if link.type = "text/css"; is really needed? I’m asking since it’s optional in HTML, since rel="stylesheet" already implies type="text/css". In which browser(s) does the script break if you omit this?

  10. Hey Stoyan,

    Your solution for Chrome and Safari seems a bit brittle. Any number of libraries could be updating the stylesheet count. Also, the load event does NOT indicate that the stylesheet is being applied to the document, unfortunately. I think this is the most important “event” to be listening for, isn’t it? Opera, Chrome, and Firefox execute the load event well before the styles are applied to any elements in the document. Finally, there are complications when loading cross-domain stylesheets (off of a CDN, for instance).

    Take a look at the isLinkReady function in the cssx/css! AMD loader plugin (for AMD loaders like RequireJS and curl.js): https://github.com/unscriptable/cssx/blob/master/src/cssx/css.js#L177

    There’s an ugly Chrome sniff+hack in there that I am trying to find a way to remove. This routine works on both same-domain and cross-domain urls as well as most browsers (FF 2+, Safari 3+, IE6+, Chrome 9+).

    I agree this is a crazy mess! Plz! More votes on those browser tickets, folks!

    – John

  11. Have you tried monitoring cssRules (for FF) in combination with checking to see if the exception code is 1000 (stylesheet loaded in FF XD)?
    https://github.com/unscriptable/cssx/blob/master/src/cssx/css.js#L205

  12. I had a similar issue, here’s my solution: http://thudjs.tumblr.com/post/637855087/stylesheet-onload-or-lack-thereof

  13. @Christos, this approach fails FF’s security policy when you load CSS from a different domain, e.g. a CDN

    @Kris, this is interesting – so being able to tell from the exception code whether:
    - cssRules is not ready yet (file not loaded)
    vs.
    - it’s ready but you “can’t touch this” because of security policy
    Is this what you have in mind?

    @Mathias, @unscriptable – yes, my code leaves to be desired, it’s more of an exploration at this stage. You should see all the detailed code review feedback I got from @jdalton :)

    @unscriptable, I see this exception 1000 in FF, very nice. I think you can replace your style sniffing in chrome with check for stylesheets.length. I’m not sure that means it hasn’t been applied, but even if not, it will be a very good approximation

  14. The HTML5 spec http://www.w3.org/TR/html5/semantics.html#the-link-element

    “Once the attempts to obtain the resource and its critical subresources are complete, the user agent must, if the loads were successful, queue a task to fire a simple event named load at the link element”

  15. @Stoyan, where the stylesheets.length test might fail is in cases in which you want to use the computed style of an element. For instance:

    div.style.height = div.scrollHeight + ‘px’;

    If some styles are not already applied to the div (because Chrome fired the load event too soon), then the calculation won’t be correct because the computed value, scrollHeight, won’t be final, yet.

    I’m going to try the stylesheets.length test later and report back. :)

  16. @Stoyan I haven’t noticed FF (3.6 or 4) having any issues with my approach. :^)

  17. What about loading CSS via XMLHTTPRequest, then populating a tag with the contents? Ugly, but you should have good control over loading.

  18. Dethe – cross-domain restrictions :(

    Christos, I replaced your function with:

    function loadStyleSheets() {
    if ( i.length ) loadStyleSheet( ‘http://tools.w3clubs.com/pagr/’ + i.shift() + ‘.sleep-1.css’, onComplete );
    }

    and got in the console:

    stylesheet http://tools.w3clubs.com/pagr/1.sleep-2.css load: failed.
    stylesheet http://tools.w3clubs.com/pagr/2.sleep-2.css load: failed.
    stylesheet http://tools.w3clubs.com/pagr/3.sleep-2.css load: failed.

    Although I can see the stylesheets load just fine in the Net panel

  19. @Emil – thanks, cool!

  20. Good write-up!

    I was wondering about a detail in the FF solution from the update:
    It’s using a try/catch-block. I’ve read some argument that using exceptions like that is very slow, because it will unwind the whole execution stack etc etc (this was probably referring to some C dialect).
    Is this of any concern in JS?
    Would it be possible, and if so, faster, to use if-statements (for example)?

    Just looking for a general opinion here, not a detailed rewrite of the code :)

  21. Stoyan, you wrote “Side note: makes me wonder – this could be a technique to preload JS without executing too”

    How can this possibly improve your existing technique for preloading CSS/JS without execution?
    http://www.phpied.com/preload-cssjavascript-without-execution/

  22. Just FYI: the load event for the <link> element is defined in HTML5:
    http://www.w3.org/TR/html5/semantics.html#the-link-element

    Once the attempts to obtain the resource and its critical subresources are complete, the user agent must, if the loads were successful, queue a task to fire a simple event named load at the link element, or, if the resource or one of its critical subresources failed to completely load for any reason (e.g. DNS error, HTTP 404 response, a connection being prematurely closed, unsupported Content-Type), queue a task to fire a simple event named error at the link element. Non-network errors in processing the resource or its subresources (e.g. CSS parse errors, PNG decoding errors) are not failures for the purposes of this paragraph.

    With it in the spec, I think (hope) that it’s just a matter of time before Firefox implements it.

  23. [...] When is a stylesheet really loaded? [...]

  24. [...] When is a stylesheet really loaded? / Stoyan’s phpied.com. [...]

  25. Nice write-up Stoyan!
    Just to contribute with the “craziness” of Magic solutions, someone suggested the following:
    - load the link css url normally (injection)
    - right after that create an image (new Image()) pointing its src to the same css url above
    - listen to image onerror event which once fired means the css file was loaded

    But someone else pointed out some issues with such solution:
    - the css file should be cached to avoid performance penalty by requesting it twice
    - if onerror fires because of a network issue then you got a false positive

    +1 vote on those browser tickets

  26. I wish this article existed a year ago. Just rewrote a bunch of code to better handle dynamic CSS loading. Thanks for the clear explanation.

  27. [...] When is a stylesheet really loaded? / Stoyan's phpied.com [...]

  28. When I need load a Dynamic CSS, I put It in a JS and check the load state by callback.
    css.js contet Example:

    writeCSS(‘h1{color:red}\
    h2{color:blue}\
    h3{color:green}’);

    loadScript(‘css.js’,function(){
    content.init();
    })

  29. [...] the implementation of onload for <link> elements is notoriously inconsistent across browsers. Plenty of browser-specific hacks exist to check if a stylesheet has loaded in a specific browser, but their longevity as browsers [...]

  30. function loadScript(src, callback) {
    var head = document.getElementsByTagName(‘head’)[0],
    script = document.createElement(‘link’);
    done = false;
    script.setAttribute(‘src’, src);
    script.setAttribute(‘type’, ‘text/css’);
    script.setAttribute(‘charset’, ‘utf-8′);
    script.onload = script.onreadstatechange = function() {
    if (!done && (!this.readyState || this.readyState == ‘loaded’ || this.readyState == ‘complete’)) {
    done = true;
    script.onload = script.onreadystatechange = null;
    if (callback) {
    callback();
    }
    }
    }
    head.insertBefore(script, head.firstChild);
    }
    loadScript(‘style.css’, function() { alert(‘style sheet loaded.’); });

  31. it’s good to have a solution for this but I wonder who really needs to load CSS from external domains in real life and, once loaded, needs to be notified.

    Also CSS may use @import as well and the fact the CSS has been loaded does not mean assets are ready ( related images, etc )

    I am pretty sure I need coffee now but can anybody tell me when is necessary to solve this problem? Many thanks :-)

  32. Andrea, my friend, coffee FTW :)

    You often need 3rd party domain when you use CDN

    @import is a strange case, yes, I haven’t investigated what happens. In HTML5 spec seems like style’s onload needs to fire when @imported stuff is loaded too (at least that’s how I read “resource and its *critical* subresources”)

    Use case: 1 page ajaxy apps.
    1. initial load
    2. click – XHR
    3. JSON response that has: {html: ”, jsfiles: [], cssfiles: []}
    4. You inject CSS files in DOM
    5. once they are loaded, you inject html too

    Otherwise user will see unstyled content. Images are less of a concern, whenever they arrive

  33. Stoyan, did you see a last comment on Zach Leatherman’s “Faking Onload for Link Elements” http://www.zachleat.com/web/load-css-dynamically/ page?
    It’s made by Daniel Buchner and points to another solution (a trick) for firing Load event for CSS: “Link element load event support for CSS Style Sheet includes, finally!” http://www.backalleycoder.com/2011/03/20/link-tag-css-stylesheet-load-event/

  34. @Vladimir, stylesheets are stored separately from images in webkit and FF. So in this case you get two downloads

  35. Support for stylesheet link element events has landed in Firefox 8, enjoy! –> https://developer.mozilla.org/en/HTML/Element/link#Stylesheet_load_events

  36. [...] doing some research and writing up my answer, I stumbled upon this link that explains everything you need to know about CSS, when it is loaded and how you can check for [...]

  37. One way to do this is check when an element has been styled by the CSS file itself. E.G, Have an element colored red by default, and then check when the CSS file itself colors it blue. setInterval is your friend here. ProTip. Have the style rule at the very bottom of the .css file so we know the whole sheet has been loaded. Haven’t wrote a script for this, but it should work in theory.

  38. Chrome Paint needs to be applied on a really positive mist.Customize Chrome purposes in your automobile or motorcycle with our Chrome spray paint Equipment. Usable for all conductive surfaces with none toxic or carcinogene supplies. Spray on Chrome for mirror – like end

  39. [...] require two different CSS files. Both modules are requested at about the same time. We listen to onload of the CSS files. Expected behavior: whenever a module and its CSS dependency arrive – show that module. [...]

  40. Seems like you actually understand a good deal pertaining to
    this subject matter and it all shows throughout this article, called “When is a stylesheet really loaded?

    / Stoyan’s phpied.com”. Thanks -Therese

  41. Fastidious answers in return of this issue
    with solid arguments and explaining all on the topic of that.

  42. Quality content is the key to attract the users to go to see
    the website, that’s what this site is providing.

  43. Hey there! Someone in my Facebook group shared this site with us so I came to check it out.

    I’m definitely enjoying the information. I’m bookmarking and will be tweeting this to
    my followers! Excellent blog and brilliant style and design.

  44. Today, I went to the beach front with my children.
    I found a sea shell and gave it to my 4 year old daughter and said “You can hear the ocean if you put this to your ear.” She put the shell
    to her ear and screamed. There was a hermit crab inside and it pinched her ear.
    She never wants to go back! LoL I know this
    is entirely off topic but I had to tell someone!

  45. I’ll immediately clutch your rss as I can not in finding your e-mail subscription hyperlink or newsletter service. Do you’ve any?
    Kindly let me recognize so that I may just subscribe. Thanks.

  46. […] 而《JavaScript Patterns》的作者Stoyan则在他的博客里,比较详细的说明了《When is a stylesheet really loaded?》。 […]

  47. I love your blog.. very nice colors & theme.
    Did you create this website yourself or did you hire
    someone to do it for you? Plz answer back as I’m looking to construct my own blog and would like to know where
    u got this from. appreciate it

  48. We have read through some great material here. Certainly amount bookmarking to get revisiting. We shock exactly how a lot effort you add for making this type of wonderful insightful site.

Leave a Reply