Chapter 4. JavaScript and CSS Over-Downloading

HTML references three primary types of resources: images, JavaScript, and CSS. While images account for the bulk of page bytes, CSS and JavaScript do carry their own weight, adding up to roughly 20% of page bytes. If we compare RWD sites to mdot sites, we can see that the number of requests and bytes required by an average RWD website is more than double those that an mdot site uses (Figure 4-1). Therefore, if we cut the JavaScript and CSS payload on a responsive site to mdot levels, we would reduce page weight by roughly 10%!

CSS and JavaScript Requests on an average RWD and mdot sites
Figure 4-1. An average RWD website requires far more JavaScript and CSS requests and bytes than its mdot counterpart

While shaving 10% of the page bytes is a worthy goal by itself, the value of such an optimization does not end there.

CSS and JavaScript Rendering Impact

Downloading an image, while not always simple, is a fairly well contained task for a browser. Since images are static, the browser can download and process many of them in parallel, without worrying about how they’ll interact with each other or with the rest of the page. Browsers do need to prioritize when and how many images are downloaded, as they handle content for network and compute cycles, but otherwise image resources are fairly autonomous. Processing CSS and JavaScript, in contrast, is far more involved.

When downloading CSS, browsers go to great lengths to avoid showing the user an unstyled page. As a result, most browsers will keep a page blank until all the CSS has been downloaded and processed, and the page can be accurately laid out. This means that getting the CSS becomes one of the most important steps in rendering a page quickly; as a result, browsers often block the downloading of other resources (primarily images) until all CSS files have been fetched. Walking the line between downloading resources in parallel and prioritizing CSS downloads is a difficult task, one that is handled differently by different browsers.

Downloading and processing JavaScript is no less complex, since any script can dynamically change a page in hugely impactful ways. For instance, scripts can inject content directly into the HTML parser, using a method called document.write(), possibly adding brand new sections to a page or commenting portions out. Other scripts may not manipulate the page, but set critical cookies that will be required by resources later on, or even navigate away from the page. Note that scripts can be marked as async, thus reducing their impact on the page load. Making scripts async is definitely the right way to go, but since doing so limits the script and interferes with execution order, it’s not always an option.

Since browsers cannot anticipate what a script will do, each script tag forces them to pause, download, and execute the external script, and only then continue. Note that each such pause means a delay in the rendering of the page as well—which means that every script tag blocks the rendering of everything that follows it in the HTML. While browsers do attempt to predictively download scripts ahead of time, the fact remains that a script’s impact on a page’s load time far outweighs its size.

Given the page-wide impact CSS and JavaScript files have on a page’s load, over-downloading such resources can have a massive impact on web page performance, far beyond the sheer number of bytes.

Problem: Hidden Single Point Of Failure (SPOF)

Another way in which JavaScript can affect page performance is by introducing risk. Consider a responsive website with a top ad banner. The site is simple—show an ad at the top, and the news below it. Here’s a snippet of its HTML:

<html>
    <head><title>Gadget News</title></head>
<body>
    <!-- Hide banner ad on small screens -->
    <style>
    @media (max-width: 480px) {
        #ad {display:none}
    }
    </style>

    <!-- Display ad -->
    <div id="ad">
        <script src="//awesomeads.com/banner.js"></script>
    </div>
    <!-- The actual news -->
    <div id="news">...</div>
</body>
</html>

Like most ads, the ad is added to the page as a synchronous script. As we’ve just explained, this means that it blocks the rendering of all the content below it—nothing will be displayed until the ad script is downloaded and executed. This may not be a big issue most of the time, but if the third-party ad service is having issues, the page may remain blank for a very long time, effectively making the entire “Gadget News” website unavailable. This performance and reliability issue is often referred to as Single Point of Failure (SPOF), as each third-party ad integrated in this manner is able to singlehandedly fail a site.

While unfortunate, this is a common pattern for a variety of reasons. Sometimes the ad network makes use of the document.write() method mentioned above, and does not support async scripts. Other times, the business believes that rendering the ads as quickly as possible is the most important step, since that’s how the website makes money. And other times, of course, it’s simply because the first or third party didn’t appreciate the risk or were not willing to make the effort to address it.

Consider, however, what happens when this HTML is loaded on a small screen. Using a Media Query, the ad is hidden from the user. This is again a common occurrence, as having less screen real estate pressures the mobile view to eliminate page parts that aren’t the core content. Elements such as ads, social streams, product reviews, and live chat buttons are often removed.

In this case, however, the ad wasn’t actually removed. Instead, it was hidden. And as we’ve mentioned before, hiding a script doesn’t make it go away. The script will still be downloaded and executed (and likely even create the actual ad), despite the fact that it’s hidden all along. The hidden script will still slow down rendering—at best delaying it and at worst, if the ad service froze, effectively taking the first-party site down.

Having a SPOF is not great, but having a hidden SPOF is simply wasteful. There’s nothing to be gained from having the script on the page in the first place, and you would likely not include it at all in a mobile-only site. As we’ve seen before, an average RWD site uses 10 more JavaScript files than its mdot counterpart. Many of these files are unnecessary, and each one adds another slight delay and an unnecessary failure point to the page.

Solution: Conditional Loading

Since excess CSS and JavaScript can slow down a site or make it less reliable, we should look for a way to only download the resources we actually need. Unfortunately, as we’ve seen with images, there’s no native way to keep browsers from downloading JavaScript and CSS resources not needed for this viewport. And as with images, the only way to truly avoid the extra download in today’s browser is with JavaScript.

Loading the right resource using JavaScript is usually referred to as conditional loading. Conditional loading is a simple concept: instead of including a JavaScript or CSS link on the page, have JavaScript add it only if necessary. A very simple conditional loader might look like this:

<script>
if (document.documentElement.clientWidth > 640) {
    document.write(
        '<script src="//ads.com/banner.js"><\/script>');
        document.write(
        '<script src="livechat.js"><\/script>');
}
</script>

The script simply queries the screen width, and adds the banner and livechat scripts only if the screen is large enough. On a small screen, the script will never be added, and thus never be downloaded nor executed. While extremely simple, this code would actually work quite well as-is. That said, there are several ways in which it can be improved:

  • Replace the document.write() method by inserting an async script element into the DOM (can only be applied to scripts that support running asynchronously).
  • Use the matchMedia() method to define the breakpoint using CSS Media Queries; this is a more standard way of defining such markers and supports using other breakpoint markers, such as ems. Be sure to use the matchMedia.js polyfill for older browsers.
  • Move the condition to be a data- attribute on the relevant tag, making it easier to maintain.

Applying all three modifications (but sticking to pixel units) results in something like this:

<script data-mq="(min-width: 640px)"
   data-src="//ads.com/banner.js"></script>
<script data-mq="(min-width: 640px)"
   data-src="livechat.js" ></script>
<script>
var scripts = document.getElementsByTagName("script");
for(var i=0;i<scripts.length; i++)
{
        // Test if the Media Query matches
        var mq = scripts[i].getAttribute("data-mq");
        if (mq && window.matchMedia(mq).matches)
        {
                // If so, append the new (async) element.
                var s = document.createElement("script");
                s.type = 'text/javascript';
                s.src = scripts[i].getAttribute("data-src");
                document.body.appendChild(s);
        }
}
</script>

This code implements all three changes, but each change can be applied independently as well. In addition, the same flow can be applied to CSS link elements by changing the filter to find relevant link elements, and changing the attribute to href. Lastly, browsers without JavaScript can be supported using <noscript> tags.

Note that this type of conditional loading does have a a couple of limitations.

First, it interferes with the preloader, just like a JavaScript-based image loader would. You’ll be well served to place all the relevant script elements in one script, as this will let the browser see them all together, and so download them in the best way it can. Note that scripts that require a specific place in the page may need to be added separately, and possibly use document.write().

Second, it’s hard to maintain script execution order when loading scripts dynamically. Where possible, I would suggest combining any scripts that need to run in order into a single file, and loading it asynchronously. You can also use the onload event of the script tag to call a specific inline function. The function will be called after the external script was downloaded and executed, allowing for some ordering even with async scripts.

Even with those limitations, conditional loading is a relatively easy way to eliminate many of the unnecessary delays and additional risks on a responsive site, caused by excess JavaScript and CSS.

Including Multi-Viewport CSS

Since much of RWD’s secret sauce lies in styling rules, it’s not surprising to see that responsive sites use more CSS files and bytes than even an average desktop website. This CSS holds the styling rules for multiple viewports, which are—by definition—more than the minimum necessary for the current viewport.

These additional rules can be included in three not-mutually-exclusive ways:

  • A single CSS file with multiple Media Queries within it
  • Separate CSS files with a Media Query in the link tag that includes them
  • Inline style tags with Media Queries

The most common practice is the first: use a small number of files (usually split by page areas), and within each, specify Media Queries to adapt to the display. For example, a menu.css file may hold the styles for all the different ways a menu may be displayed as screen size changes. This approach is often developer/designer friendly, but it carries with it a performance penalty. Since the Media Queries are inside the CSS file, the browser has no choice but to download and parse the entire file before ignoring the unmatched Media Queries.

The second approach, while less popular, performs better, since it informs the browser of the Media Query each CSS file applies to before downloading the file. Most browsers will still download this CSS file (for various reasons, some better than others), but will likely defer the download to the end of the page to keep such files from blocking rendering. Using conditional loading, as mentioned above, can eliminate the download completely.

Lastly, the third approach, inlining the style rules, is not very commonly used. Inlining is a good page acceleration technique for first-time visitors, but it gets in the way of caching the CSS files, slowing down the subsequent page views. While powerful, inlining should only be used for very small objects or for content that is critical to the page load. Unless your systems are able to inline only the critical content for this viewport, or are using automated front-end optimization tools to do so for you (e.g., Akamai’s Ion or Google’s mod_pagespeed), I would not recommend inlining your CSS.

Summarizing Responsive JavaScript and CSS

Over-downloading of JavaScript and CSS impacts the user experience far more than what the byte reduction implies. To avoid over-downloading and maximize reliability, I would suggest that you:

  • Make as many scripts asynchronous as you can; this makes it easier to conditionally load and imposes less risk on the page.
  • Split your CSS files by breakpoint, and include Media Queries on the link tag itself to inform the browser when to use them.
  • Use conditional loading to avoid excess downloads, as even async scripts and media-tagged links would be downloaded, costing time and bandwidth.

Get Responsive & Fast now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.