Debunking Responsive CSS Performance Myths

Responsive design is our best answer today to the explosion and the variety of the different screen sizes on which online content is consumed: smartphone resolutions vary widely, landscape and touch orientations, different pixel densities, and so forth. CSS3 media queries allow the browser to alter the presentation of the page based on all of the above attributes and more without making us modify the underlying HTML of the page - nice win.

However, when it comes to performance, we still have a lot of kinks to work out with responsive design. For one, while we can optimize the presentation on the client, most of the sites do not optimize the actual assets: you may be viewing a mobile version of the site, but you are likely downloading the same desktop assets. Not a great story for mobile.

eCSSential: optimized CSS loading

With performance in mind, Scott Jehl put together eCSSential, which aims to optimize loading of CSS assets:

eCSSential is a JavaScript utility that is designed to make browsers download files in a faster, more responsible manner than they do by default.. Using separate link elements with media attributes to reference stylesheets with their intended breakpoints doesn't prevent those stylesheets from downloading and blocking page rendering, even in environments where they don't currently or will never apply.

Technically speaking, it is a tiny bit of JavaScript that when placed in the head of a page, determines which of your stylesheets should be loaded immediately and block page rendering (any stylesheets intended for mobile-first breakpoints that currently apply), which stylesheets should be deferred to load asynchronously (any stylesheets intended for breakpoints that don't currently apply to the current viewport size, but could apply later, given the device's screen size), and which stylesheets should never be loaded at all (any stylesheets intended for viewport dimensions that are larger than the device's screen).

Sounds great, except that eCSSential is trying to solve problems that don't exist and in the process only adds additional overhead - it is a performance antipattern.

Myth #1: All stylesheets block rendering

The browser must wait until it downloads, parses, and applies the relevant CSS before it can render the page on the screen. Not respecting this will lead to a "flash of unstyled content" (FOUC), which would create a poor user experience. However, modern rendering engines are much smarter than many of us give them credit for. Let's take a look at WebKit's implementation of HTMLLinkElement:

// simplified version, with relevant bits to this discussion
void HTMLLinkElement::process()
{
  if (m_disabledState != Disabled
        && (m_relAttribute.m_isStyleSheet || (acceptIfTypeContainsTextCSS && type.contains("text/css")))
        && document()->frame() && m_url.isValid()) {

    bool mediaQueryMatches = true;
    if (!m_media.isEmpty()) {
      mediaQueryMatches = evaluator.eval(media.get());
    }

    // Don't hold up render tree construction and script execution on stylesheets
    // that are not needed for the rendering at the moment.
    bool blocking = mediaQueryMatches && !isAlternate();
    addPendingSheet(blocking ? Blocking : NonBlocking);

    // Load stylesheets that are not needed for the rendering immediately with low priority.
    ResourceLoadPriority priority = blocking ? ResourceLoadPriorityUnresolved : ResourceLoadPriorityVeryLow;
    document()->cachedResourceLoader()->requestCSSStyleSheet(request, charset, priority);
  }
}

The code should speak for itself. If the stylesheet is marked as disabled, we don't download it. Next, if the link element provides a media query, then it is evaluated immediately: if the media query evaluates to false then the stylesheet is marked as NonBlocking and is given a very low download priority. Here is an example scenario:

<!-- blocking stylesheet, nothing renders until it is downloaded and parsed -->
<link href="main.css" rel="stylesheet">

<!-- non-blocking, low download priority because of the evaluated media query -->
<link href="i-want-a-monitor-of-this-size.css" rel="stylesheet" media="(min-width: 4000px)">

<!-- won't be downloaded at all, because it is marked as disabled -->
<link href="noop.css" rel="stylesheet" disabled>

<!-- print stylesheet is non-blocking -->
<link href="noop.css" rel="stylesheet" media="print">

Given the above, only main.css will block the rendering of the page. Media queries on link elements are you friend, use them. The only caveat, as Scott indicates, is that the browser will download all enabled stylesheets, even though the screen on your device may not ever exceed the 4000px width. This is a possible area of improvement in WebKit: evaluate whether the constraint can be satisfied at all on the current device.

Myth #2: CSS is always in the critical path

Scott is right, loading CSS in an optimized and prioritized fashion is difficult. However, you won't do any better by trying to outsmart the browser from client-side JavaScript. In the example above I marked the media="print" stylesheet as non-blocking, how come? We already know half of the answer: if the media query does not match, we mark it as non-blocking and give it a low resource priority. Now let's take a look at WebKit's HTMLPreloadScanner:

void preload(Document* document, bool scanningBody, const KURL& baseURL)
{
    CachedResourceLoader* cachedResourceLoader = document->cachedResourceLoader();
    ResourceRequest request = document->completeURL(m_urlToLoad, baseURL);

    if (m_tagName == linkTag && m_linkIsStyleSheet && m_linkMediaAttributeIsScreen)
        cachedResourceLoader->preload(CachedResource::CSSStyleSheet, request, m_charset, scanningBody);
}

static bool linkMediaAttributeIsScreen(const String& attributeValue)
{
    // Only preload screen media stylesheets. Used this way, the evaluator evaluates to true for any rules
    // containing complex queries (full evaluation is possible but it requires a frame and a style selector
    // which may be problematic here).
    MediaQueryEvaluator mediaQueryEvaluator("screen");
    return mediaQueryEvaluator.eval(mediaQueries.get());
}

Preload scanner only fetches stylesheets with screen device type, which is the default type. Hence, tv, print, and other non-screen stylesheets won't compete with other blocking resources required to render the page. Further, injecting stylesheets through JavaScript would only get in the way of Chrome's network predictor and asset DNS caches.

Mobile Networks: it's complicated

We've already established that the browser is smart enough to defer the loading of stylesheets which (a) do not match the media query, and (b) do not match the media device type. All other stylesheets are given a PriorityVeryLow to allow other critical assets, like JavaScript, to go ahead in the download queue. But, couldn't we do better by requesting some of the stylesheets on demand?

Somewhat counter-intuitively, that would likely lead to a worse off experience due to the cost of the setup of a mobile networking link. First off, if the device has been idle for a few seconds, the radio is turned off. To send a request, we enable the radio and wait (1-2s), after that we perform the TCP handshake (plus DNS lookup, if required), which is another 200-1000ms, and then we download the content: 2~4 seconds in, we have our stylesheet. Are you willing to wait that long when you are resizing the screen, or loading a print preview?

Given that the size of CSS assets is just 37kB on average, you are much better off downloading all of the CSS content in one shot over the initial, warmed up connection. By doing so, you will also help the user extend their battery life. If there is one optimization to be made here then it is simply: concatenate and compress.

Outsmarting the Browser

A modern browser performs a complicated dance around every page load: DNS prefetching and preconnect, speculative resource loading, applying historical usage patterns, and more. While there is always room for improvement, it is highly unlikely that you will outsmart it in a client-side library - let the browser optimize resource loading, it is already very good at it, and file tickets where the behavior could be improved.

Concatenate and compress your CSS, use media queries in your link tags and let the browser do its job. Given that an average site today serves 37kB of CSS, which is less than 5% of the 1066kB total, chances are you will find much larger gains elsewhere. Optimizing images, which account for over 60%+ of the size is a good start.

Ilya GrigorikIlya Grigorik is a web ecosystem engineer, author of High Performance Browser Networking (O'Reilly), and Principal Engineer at Shopify — follow on Twitter.