When CSS Blocks

#

The other day I was auditing a site and ran into a pattern that I’ve seen with a few different clients now. The pattern itself is no longer recommended, but it’s a helpful illustration of why it’s important to be careful about how you use preload as well as a useful, real-world demonstration of how the order of your document can have a significant impact on performance (something Harry Roberts has done an outstanding job of detailing).

I’m a big fan of the Filament Group—they churn out an absurd amount of high-quality work, and they are constantly creating invaluable resources and giving them away for the betterment of the web. One of those great resources is their loadCSS project, which for the longest time, was the way I recommended folks load their non-critical CSS.

While that’s changed (and Filament Group wrote up a great post about what they prefer to do nowadays), I still find it often used in production on sites I audit.

One particular pattern I’ve seen is the preload/polyfill pattern. With this approach, you load any stylesheets as preloads instead, and then use their onload events to change them back to a stylesheet once the browser has them ready. It looks something like this:

<link rel="preload" href="path/to/mystylesheet.css" as="style" onload="this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="path/to/mystylesheet.css"></noscript>

Since not every browser supports preload, the loadCSS project provides a helpful polyfill for you to add after you’ve declared your links, like so:

<link rel="preload" href="path/to/mystylesheet.css" as="style" onload="this.rel='stylesheet'">
<noscript>
    <link rel="stylesheet" href="path/to/mystylesheet.css">
</noscript>
<script>
/*! loadCSS rel=preload polyfill. [c]2017 Filament Group, Inc. MIT License */
(function(){ ... }());
</script>

Network Priorities Out of Whack

I’ve never been super excited about this pattern. Preload is a bit of a blunt instrument—whatever you apply to it is gonna jump way up in line to be downloaded. The use of preload means that these stylesheets, which you’re presumably making asynchronous because they aren’t very critical to page display, are given a very high priority by browsers.

The following image from a WebPageTest run shows the issue pretty well. Lines 3-6 are CSS files that are being loaded asynchronously using the preload pattern. But, while developers have flagged them as not important enough to block rendering, the use of preload means they are arriving before the remaining resources.

Blocking the HTML parser

The network priority issues are enough of a reason to avoid this pattern in most situations. But in this case, the issues were compounded by the presence of another stylesheet being loaded externally.

<link rel="stylesheet" href="path/to/main.css" />
<link rel="preload" href="path/to/mystylesheet.css" as="style" onload="this.rel='stylesheet'">
<noscript>
    <link rel="stylesheet" href="path/to/mystylesheet.css">
</noscript>
<script>
/*! loadCSS rel=preload polyfill. [c]2017 Filament Group, Inc. MIT License */
(function(){ ... }());
</script>

You still have the same issues with the preload making these non-critical stylesheets have a high priority, but just as critically and perhaps a bit less obvious is the impact this has on the browsers ability to parse the page.

Again, Harry’s already wrote about what happens here in great detail, so I recommend reading through that to better understand what’s happening. But here’s the short version.

Typically, a stylesheet blocks the page from rendering. The browser has to request and parse it to be able to display the page. It does not, however, stop the browser from parsing the rest of the HTML.

Scripts, on the other hand, do block the parser unless they are marked as defer or async.

Since the browser has to assume that a script could potentially manipulate either the page itself or the styles that apply to the page, it has to be careful about when that script executes. If it knows that it’s still requesting some CSS, it will wait until that CSS has arrived before the script itself gets run. And, since it can’t continue parsing the document until the script has run, that means that stylesheet is no longer just blocking rendering—it’s preventing the browser from parsing the HTML.

This blocking behavior is true for external scripts, but also inline script elements. If CSS is still being downloaded, inline scripts won’t run until that CSS arrives.

Seeing the problem

The clearest way I’ve found to visualize this is to look at Chrome’s developer tools (gosh, I love how great our tools have gotten).

In Chrome, you can use the Performance panel to capture a profile of the page load. (I recommend using a throttled network setting to help make the issue even more apparent.)

For this test, I ran a test using a Fast 3G setting. Zooming in on the main thread activity, you can see that the request for the CSS file occurs during the first chunk of HTML parsing (around 1.7s into the page load process).

For the next second or so, the main thread goes quiet. There are some tiny bits of activity—load events firing on the preloaded stylesheets, more requests being sent by the browser’s preloader—but the browser has stopped parsing the HTML entirely.

Around 2.8s, the stylesheet arrives, and the browser parses it. Only then do we see the inline script get evaluated, followed by the browser finally moving on with parsing the HTML.

The Firefox Exception

This blocking behavior is true of Chrome, Edge, and Safari. The one exception of note is Firefox.

Every other browser pauses HTML parsing but uses a lookahead parser (preloader) to scan for external resources and make requests for them. Firefox, however, takes it one step further: they’ll speculatively build the DOM tree even though they’re waiting on script execution.

As long as the script doesn’t manipulate the DOM and cause them to throw that speculative parsing work out, it lets Firefox get a head start. Of course, if they do have to throw it out, then that speculative work accomplishes nothing.

It’s an interesting approach, and I’m super curious about how effective it is. Right now, however, there’s no visibility into this in Firefox’s performance profiler. You can’t see this parsing work in their profiler, whether that work had to be redone and, if so, what the performance cost was.

I chatted with the fine folks working on their developer tools, though, and they had some exciting ideas for how they might be able to surface that information in the future—fingers crossed!

Fixing the issue

In this client’s case, the first step to fixing this issue was pretty straightforward: ditch the preload/polyfill pattern. Preloading non-critical CSS kind of defeats the purpose and switching to using a print stylesheet instead of a preload, as Filament Group themselves now recommend, allows us to remove the polyfill entirely.

<link rel="stylesheet" href="/path/to/my.css" media="print" onload="this.media='all'">

That already puts us in a better state: the network priorities now line up much better with the actual importance of the assets being downloaded, and we’ve eliminated that inline script block.

In this case, there was still one more inline script in the head of the document after the CSS was requested. Moving that script ahead of the stylesheet in the DOM eliminated the parser blocking behavior. Looking at the Chrome Performance panel again, the difference is clear.

Whereas before it was stopped at line 1939 waiting for the CSS to load, it now parses through line 5281, where another inline script occurs at the end of the page, once again stopping the parser.

This is a quick fix, but it’s also not the one that will be the final solution. Switching the order and ditching the preload/polyfill pattern is just the first step. Our biggest gain here will come from inlining the critical CSS instead of referencing it in an external file (the preload/polyfill pattern is intended to be used alongside inline CSS). That lets us ignore the script related issues altogether and ensures that the browser has all the CSS it needs to render the page in that first network request.

For now, though, we can get a nice performance boost through a minor change to the way we load CSS and the DOM order.

Long story short:

  • If you’re using loadCSS with the preload/polyfill pattern, switch to the print stylesheet pattern instead.
  • If you have any external stylesheets that you’re loading normally (that is, as a regular stylesheet link), move any and all inline scripts that you can above it in the markup.
  • Inline your critical CSS for the fastest possible start render times.