On a recent project we were tasked with improving the performance of a suite of websites, here are some of the options we looked at along the way. There are loads of factors which influence website performance; images, fonts, lazy-loading, protocol, CDN, caching, GZIP, file sizes, CSS selector complexity, server response times to list a small fraction, for now I’m going to look at options for loading the JS and CSS assets.

To get the best performance out of our websites we need to decide which metrics are most important to focus on, for us this was the FMP (First Meaningful Paint) and DCL (DOMContentLoaded) events specifically. These were our main targets for improvement and both of these are a good place to start for most websites. Although, having a good understanding of what the business goals are on the site will help determine which metrics are most important to focus on.

Where we started from...

Both the JS and CSS were synchronous requests, the JS was packaged in two files, one prerequisite JS script tag in the head, and one main JS script tag located at the end of the body both with src attributes linking to files. The main JS contains quite a number of scripts that initiate calls to third party scripts via AJAX, these where run immediately. The CSS was packaged into one file and added via a link tag in the head. It contained all the media query styles including the print styles.

What to load and what to defer?

With the CSS we had to determine what is on the critical rendering path and what can be deferred (for more info see Google’s critical rendering path tutorial). As for the JS, this was more about identifying what, if anything, MUST be loaded up front, if nothing then all the JS can be deferred.

CSS

In an ideal world we need to extract the critical path CSS from the main body of the CSS, this is easier said than done though, to start with, what constitutes “above the fold" isn’t immediately obvious! But we have to start somewhere, so we created a critical CSS file and started adding styles that the browser would obviously need to render the page e.g. grid, header component, hero, main navigation styles etc... There are automated approaches like Critical or Penthouse, but for us these were not appropriate, instead we basically divided the main SASS file into two, critical and everything else, and put the appropriate import statements into the relevant file.

Once we had separated the critical path CSS, we kept this as a standard render-blocking link tag in the header, ideally we would like to inline it in a style tag in the head, but this is still on the TODO list at the moment! The remaining non-critical CSS could then be safely deferred.

High priority CSS: grid, above the fold element styles etc... Low priority CSS: any dynamic elements, below the fold element styles, media queries (more on media queries later) etc…

High priority CSS

Load StrategyExampleComment

Inline

Style element in head

<head>
  ...
  <style>
     /* css here */
  </style>
  ...
</head>

This would be the best option as far as performance is concerned

Pros: No render blocking request needed at all

Cons: Cache management more complex. Tooling may be required to extract the critical path CSS. Implementation may require development to core system. Adds to page weight.

Synchronous

Link element in the head

<head>
  ...
  <link rel="stylesheet" href="...">
  ...
</head>

This is the default CSS implementation. Browsers won’t start to render any HTML after this tag until it's contents have been received and processed

Pros: Easy to manage (version number, cache control etc...)

Cons: Adds a render-blocking request, will negatively impact on all performance metrics

Low priority CSS

Presuming we have extracted the critical path CSS and use one of the above methods to include it, the following methods can be applied to the remaining low priority CSS

Load StrategyExampleComment

Synchronous

Link element positioned at the end of Body element

<body>
    ...
    <link rel="stylesheet" href="...">
</body>

Having a normal link tag just before the closing body tag won’t block rendering, but probably less efficient than having an async request in the head.

Pros: Easy to implement, not render-blocking, no JS shenanigans needed

Cons: Requested with highest-priority, more efficient options exist

For an indepth look at CSS delivery see this CSSWizardry article

Preload

rel="preload" and as="style" attributes

For more see Moz Preload

<link
    as="style"
    rel="preload"
    href="..."
    onload="this.rel='stylesheet'">

Requests the stylesheet asynchronously high-priority, critically this is a non-render blocking request unlike normal rel="stylesheet" link so this could be put in the head and likely get better results that the default link at the bottom of the body tag

Cons: Requires JS, and JS polyfill to work due to patchy browser support see Can I Use, preload?

Alternate stylesheet

rel=”alternate stylesheet”

For more see Moz Altstyles w3 Alternatives

<link
    rel="alternate stylesheet"
    href="..."
    title=""
    onload="this.rel='stylesheet'">

Alternative stylesheets are usually requested asynchronously low-priority. Some browsers offer a menu of alternative styles if they exist, this method removes the ‘alternate’ keyword from the rel attribute when the CSS loads. This is likely to get better results than the default link at the bottom of the body tag.

Cons: Requires JS to work

The media="none" hack

<link
    rel="stylesheet"
    media="none"
    href="..."
    onload="this.media='all'">

This is likely to behave similar to ‘alternate’ a low-priority, non-render blocking request. This method is often used as a fallback for the previous rel=“preload" example for browsers that don’t support the ‘preload’ value on the ‘rel’ attribute.

Cons: Requires JS to work.

By Media Query

Multiple Link elements with separate media query values

<link
    rel="stylesheet"
    media="(min-width: 768px)"
    href="...">
<link
    rel="stylesheet"
    media="(min-width: 1000px)"
    href="...">
<link
    rel="stylesheet"
    media="(min-width: 1200px)"
    href="...">
<link
    rel="stylesheet"
    media="(min-width: 1600px)"
    href="...">
<link
    rel="stylesheet"
    media="print"
    href="...">

Instead of having a single CSS file containing multiple ‘@media’ declarations, split the CSS into different files based on the media query and have separate link elements in the HTML for each file. This allows the browser to download the CSS files that match the current media query immediately, whilst still fetching the other files with a low priority non-blocking request.

With HTTP/1 individual requests had quite an overhead so reducing the number of requests was desirable. In HTTP/2 this isn’t such a concern.

Pros: good performance, good user experience

Cons: may be difficult to implement, change to tooling around how CSS is built.

With the critical path CSS separated, the page is now a little vulnerable to displaying unstyled content to the user if they scroll down before the main CSS has loaded. To mitigate this our default approach is to hide these elements in the critical path CSS and then unhide them in the main CSS. This prevents unstyled components displaying at all, true it’s a bit of a sledge hammer approach, but we still have the option to fine tune the pre-load state in future with skeleton place holders or similar. Using JS script to load low priority CSS For the link tag rel attribute ‘preload’ polyfill you need to write a fallback or use a tried and tested script such as CSSRelPreload.js to provide browser support, or if you want to defer loading the CSS until page OnLoad then write a small script (remembering to retain a no-script version so whatever happens the browser will always download all the CSS).

JavaScript

We already started with a distinction between what MUST be loaded in the head and what could be deferred. As with the CSS it would be great to inline the ‘must have’ JS with a script tag in the head but this would require development work we didn’t have bandwidth for at the time, so we put this task on the ever growing TODO list! The main JS was a synchronous script tag located just before the closing body tag.

Essential to load in the head: e.g. analytics, polyfills etc... Low priority*: e.g. main js, third party scripts

*Low priority does not mean ‘non-essential’, these scripts may well be an integral part of the website, we still want to load these in the most efficient order to get the best performance possible

High priority JS

Load StrategyExampleComment

Inline

Inline script element in head

<head>
  ...
  <script>
    ...
  </script>
  ...
</head>

Pros: no http request needed, runs immediately

Cons: Versioning and cache management more complex. Maybe difficult to implement. Adds page weight if huge.

Sync

Script element with ‘src’ attribute in the head

<head>
  ...
  <script src="..."></script>
  ...
</head>

Pros: Easy to manage (version number, cache control etc...)

Cons: Adds a render-blocking request

Low priority JS

Presuming high priority JS has been loaded using one of the above methods, the following methods can be applied to the remaining low priority JS.

Load StrategyExampleComment

Async

Script element with ‘async’ attribute in the head

<head>
  ...
  <script async src="..."></script>
  ...
</head>

In terms of a script tag in the source HTML I can’t think of any use-case where ‘async’ would be better than ‘defer’. Adding async is unlikely to have anywhere near as much improvement to any metric when compared to defer.

NB - Async is very useful when programmatically requesting new JS/JSON files

Defer

Script element with ‘defer’ attribute in the head

<head>
  ...
  <script defer src="..."></script>
  ...
</head>

This will cause the JS to be run when HTML parsing is complete. Also the browser will run scripts in the order they are specified in the HTML. Adding this will usually improve FMP and DCL events

Sync

Script element at bottom of body element

<body>
    ...
    <script src="..."></script>
</body>

A fairly common paradigm, adds a blocking requests at the bottom of the page where it’s least obtrusive

Similar to 'defer', but probably less efficient.

Onload

Using a script to load the JS file OnLoad

<script
    data-src="..."
    class="lazy-js"></script>
<script>
window.addEventListener("load", function() {
  var doc = document;
  var els = doc.querySelectorAll(".lazy-js");
  els.forEach(function(el) {
    var script = doc.createElement("script");
    var parent = el.parentNode;
    script.async = true;
    script.src = el.getAttribute("data-src");
    parent.insertBefore(script, el);
    parent.removeChild(el);
  });
});
</script>

Loading low priority and third-party scripts on page OnLoad event will most likely lead to a good improvement to pretty much all the performance metrics.

Considerations; are you visually showing controls that require JS to work? If so these need to be disabled until the JS has initialised. This sounds obvious, but when you move a script from loading sync in the head to run OnLoad you may find there are a few controls than never needed a disabled state that suddenly do!

We used a small script to load the main JS when the OnLoad event fires. Also, we added a feature in the CMS giving us the ability to switch this between sync, defer and OnLoad. This ability was added for future proofing, if any scripts needing to run before page load were added or if there were any unforeseen issues with running the main JS after page load, admins could change this setting to ‘defer’ or even back to ‘sync’ in the CMS.

Round-up

So all in all we didn’t make that many changes, now the main CSS and the main JS are added when the OnLoad event fires, the overall performance improvements where roughly; Full page load by 22%, DCL improved by 26% and FMP improved by 36% which helped us to hit our performance targets. Also, harder to measure, but the general user experience was improved as we were prioritising the things the user was most likely to interact with.

Historically, relying on the OnLoad event to fire could be a bit risky, the danger being that a third-party script will dynamically request a CSS or image asset which is on a server somewhere that fails to respond, the browser will then only fire the OnLoad event when the request eventually times out. By loading all the third-party scripts after the page OnLoad event has already fired helps to mitigate this issue.

We also have the improvements in our TODO list, such as inlining the critical CSS and head JS, which would improve the FMP metric. Also, splitting the CSS into individual files by media query and adding them is separate link elements would likely improve the user experience. All in all, we are in a better place, now we are meeting our performance targets, the pressure is reduced somewhat and going forward we can focus on improving the user experience whilst maintaining the current performance.