link elements block DOM parsing too

Today we’re pretty well versed with how JavaScript works. We know that script elements block rendering (well, actually blocks the parsing, which thus delays the rendering), and we know why8. Sure, so we put the script elements at the end of the document. But did you know that link elements block too?

* document.write can affect the DOM tree, so the parsing makes sure these happen synchronously

Sure I did, so what?

In the past it wasn’t likely that you would include an externally linked CSS asset, but that’s changed today. Today we have services providing fonts, at that point you’re linking to an external service from your head element (because we put CSS in the head, and script at the end of the body).

If that service hangs for whatever reason, it’ll hang your page too. Something we’ve spent a long time in the JavaScript community working to avoid, and now we risk repeating ourselves.

Why does this happen?

What’s a little frustrating is that I can completely understand why a script element would block parsing the DOM (and thus block rendering), but I can’t see why an external stylesheet would.

Perhaps it’s because we could include dynamic content via CSS – but I doubt it. Dynamic content doesn’t actually appear in the DOM tree, so I’d guess that it’s not blocking the parsing process. So what else could it be? Suggestions in the comments please!

Perhaps this is a bug? Safari, Chrome and Firefox suffer from this issue. Opera doesn’t (but then, does it also block on JavaScript – I have a feeling it doesn’t). I didn’t test IE (partly because I was sure it would, partly because I didn’t want to start my VM – if someone could confirm, that would be super).

Right now, I’m not sure what’s at the root cause, but I do know it’s putting some web sites at risk.

Updated 12-June 2011 after further investigation by zcorpan (aka Simon Pieters) and Stepan Reznikov (via their comments below), what we’re actually seeing is render blocking, and not parsing blocking.

However, it does, from looking at tests, block JavaScript after the hanging link element from running – which is definitely weird.

There’s two example for you – both need the console open: hang example where content ready fires before CSS has loaded, hang example where script waits for CSS before it can run

Example

This url will show the hanging: http://jsbin.com/agumu4/3/ – make sure you have a web console open and refresh to watch the state change.

<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8 />
<title>Hang test</title>
<script>
// script in head to debug state change
console.log('doc state: ' + document.readyState);

document.onreadystatechange = function () {
  console.log('doc state change: ' + document.readyState);
};
</script>
<link href="http://hang.nodester.com/hang.css?5000" rel="stylesheet" />
</head>
<body>
  <p>Hello World</p>
</body>
</html>

Testing

I’ve created a simple hanging service that you can reuse yourself. It’s running on Node so there’s no worry of nuking the machine due to the hang (as opposed to using PHP to test using a sleep – which would nuke a public machine).

To test include the following url: http://hang.nodester.com/file.type?ms

i.e. http://hang.nodester.com/foo.css?2000 will return a file with a CSS mime type and hang for 2000 milliseconds.

The best way to determine when the DOM is loaded (or loading) when you can’t see the output is to listen for the readystatechange event (ht). So in my test, I’ve included some script that tells me where the DOM is up to, and I can visually confirm whether the link element is hanging.

What about font includes?

I also tested including hanging fonts via CSS, i.e. if you copied the @font-face declarations, but the font service was down: this does not hang the page. However, you do suffer from the FOUC, but that’s a whole different issue.

The fix

As per anything that hangs: do it asynchronously to the DOM rendering – or rather after the DOM has done most, if not all of the parsing. That is, to use JavaScript to insert the link element once the DOM – or rather content is ready. Here’s a simple example: http://jsbin.com/agumu4/4 – note that the readystate says it’s still loading, that’s because my DOM doesn’t fully load when the JavaScript appends the link element (i.e. it doesn’t wait for an event, it’s just at the bottom of the document).

13 Responses to “link elements block DOM parsing too”

  1. It doesn’t block parsing. It blocks scripts (since the script could be asking for layout information) and it blocks rendering (since you don’t want to show unstyled content). I guess DOMContentLoaded would fire before the stylesheet has loaded, and you could also include an image and see in the network log that it will load before the stylesheet (images aren’t speculatively loaded IIRC).

    Opera does block on scripts, unless delayed script execution is enabled (it is with Opera Turbo).

  2. I always understood that CSS could effect the layout of the page so to give the browser as much time as possible to do all the calculations for margins, paddings, positioning etc. the rule was you had to serve you CSS up front in the head. “Back in the day” all that processing was super intensive so any shortcuts you could do to make it easier where just the way of things, without it you ended up with half styled content jumping around add more and more styles came down the wire (especially if the site had CSS that was not in the head or images without width/height).

    There is the other issue of if a stylesheet includes another then the order of how those styles are applied would/could matter so as a result you’d have to wait until each of the styles was pulled down and process them in order.

    So if you had 3 stylesheets (A, B and C) and A included B (using an @import) you’d always want it to render out as A -> B -> C and never A -> C -> B. The simple way to stop that is to block I guess. Anyway, I’ve done no tests on this just my thoughts on how it always worked.

  3. @zcorpan – my test doesn’t agree. Check the console log in something like Chrome. You’ll see the readyState on loading, then hang for 5 seconds, then say interactive, then say complete – and complete, by my understanding, is when the DOM is parsed and ready.

    If you test the same situation but with DOMContentLoaded listening too, you’ll see the console hang after loading, then interactive, then DOMContentLoaded, then complete: http://jsbin.com/agumu4/5/

    Which is why I’m thinking that the hanging link is blocking.

  4. @Pete Duncanson – layout: yes, DOM tree: no. And it’s the later that’s the reason (I’m saying) that it’s blocking.

    DOM tree changes due to document.write are the reason script elements block. Which is why it’s odd that link elements are blocking.

  5. It’s the combination of a stylesheet followed by an inline script that’s causing the trouble. The inline script might contain code that depends on the styles applied from the stylesheet, so browsers download the stylesheet and execute the inline script sequentially in order to guarantee reproducible results.

    If you remove scripts below the stylesheet, then “doc state change: interactive” and “content ready” are fired immediately, NOT after 5 seconds hang: http://www.stepanreznikov.com/playground/tests/external-css.html

  6. @rem – I tried in IE9 (and in it’s IE8 and IE7 modes) in Windows 7, and each did “hang” between the interactive and complete states.

    My first though getting into this post was that it was support for pseudo classes like :before and :after pseudo classes that may cause browsers to hang for CSS in case the CSS generated content. But that doesn’t account for IE7 (at least within IE9 developer tools) having the hang as well as according to: http://www.quirksmode.org/css/contents.html IE7 doesn’t support :before and :after. So as you seem to feel, it’s likely something else.

  7. Ok so in Chrome readyState and DOMContentLoaded waits for the stylesheet. (It doesn’t in Opera.) That doesn’t mean that parsing is blocked. Here’s a test:

    data:text/html,<link href=”http://hang.nodester.com/hang.css?5000″ rel=”stylesheet”><img src=http://google.com/404 onerror=alert(1)>

  8. The page hangs in IE9, too, so the fix should be used in that browser, as well.

  9. You say that <link> elements block rendering, but what about <style> elements? For example would it fix the issue to replace <link src="my.css"> with <style>@import "my.css";</style>?

  10. Okay, I need to update this post, Stepan’s comment above, with a bit of tweaking shows that indeed the link element is not blocking parsing but only blocking the rendering.

    I’ve updated it to try to get some of the DOM when the content loaded event fires – you’ll see, with the console open, that the contents of the p element is echoed, yet the rendering is hanging: http://remysharp.com/demo/hang.html

    Thank you Zcorpan and Stepan for getting us to the point where we’ve got a good understanding of this effect. I’ll update the post shortly.

    @Richard – I’ll ad a test for the @import test too.

  11. [...] link elements block DOM parsing too (not just script tags!) [...]

  12. Thanks for this post. Gotta stay on our toes with this stuff.

    btw, in the very first paragraph the second sentence ends with an 8 instead of the *. And since this comment doesn’t add much you can just leave it out. I won’t be bummed.

  13. Hey Remy. Sorry for posting to this old thread but I’m currently investigating docwrite and was looking for blocking elements. Found your blog post and discovered an interesting addendum; if you docwrite an external stylesheet it won’t block the current script thread or even future or docwritten script tags. However, it would block future static script tags. +1 for fubar.

Leave a Reply
Not required

CODE: Please escape code and wrap in <pre><code>, doing so will automatically syntax highlight