But have we come to rely on it too much?
Later, we got the async and defer attributes. Async means that the page can continue to be built while the script loads. Once the script has loaded, it is executed immediately (meaning that execution order of different scripts on the page can’t be guaranteed). Defer works in much the same way, except that execution waits for the DOMContentLoaded event, and any dependencies based on how the scripts are ordered in the document are preserved.
But what about all our asynchronous and deferred scripts? Surely, they can’t detract from the user experience?
Why asynchronous scripts can still be ‘blocking’
Unfortunately, browsers don’t know this. When it comes to the order in which objects are loaded, browsers tend to give higher priority to scripts than they do to images (scripts might, after all, be required for the page to work).
But what if you have a large number of scripts and images? The scripts might be important because they add a number of enhancements to the page. But maybe the images matter more to you – and, more importantly, to the people who come to your website.
Despite this, the browser may stubbornly stick to loading the scripts early, even if you’ve put them at the bottom of the page, after all your images.
Here’s an example. This test page contained a lot of images, referenced towards the top of the document. It also contained a lot of scripts, referenced at the bottom. We tested it in Chrome, using WebPagetest.
Half the images loaded ahead of the scripts. However, the other half, which included some above-the-fold pictures, loaded afterwards.
It’s also helpful to look at the connection view waterfall, which shows script files tying up connections ahead of images:
Is there anything we can do about this?
One partial solution is to avoid loading large numbers of scripts and images from the same domain. Delivering them from separate domains will at least mean that images won’t have to ‘queue’ behind scripts on the same connection.
However, even if you move all your scripts to a different domain, there’s still a limit to the total number of connections a browser will open. And there’s no guarantee that the next set of connections will be given over to your images. Instead, they could easily be occupied by third-party scripts, for example.
The disadvantage of this is that scripts loaded in this way can’t be discovered by the browser preloader. So, yes, they’ll be loaded after the images, but they might just be loaded too late for our liking. The same applies to another possible alternative – loading scripts on the onload event.
What we really need to do is change the priority the browser gives to images, so that it leapfrogs the priority level given to scripts.
Fortunately, we now have a way to do this, using preloading. Preloading lets us tell the browser to load certain resources that are going to be needed on the page. It is particularly useful for the early loading of objects that tend to be discovered late, such as fonts and background images referenced in CSS.
However, it also allows us to alter the priority given to those objects.
Preload is used within a <link> element, as follows:
<link rel="preload" href="images/mypic.jpg" as="image">
The as attribute indicates the kind of object the browser should expect. However, it’s not very helpful for our purposes. This is because telling the browser to expect an image will lead to it having the same low priority as it did before.
Instead, we can simply omit the as attribute:
<link rel="preload" href="images/mypic.jpg">
This doesn’t give the image especially high priority, but it does elevate it above the default priority for scripts.
Here are the waterfall charts for our test page when we tried this:
And the connection view:
There are two points to bear in mind about using preload in this way.
The first is that support, at the time of writing, is limited to Chrome, Opera and Android Browser (source:caniuse.com).
The second is that it’s quite a blunt instrument. We’re taking advantage of the fact that the default priority for preloaded objects is higher than it is for scripts.
Instead, it would be useful to have finer control over the priority given to each object on the page, rather than leaving it to the browser to guess at what’s most important. We started to see something like this with the now-abandoned Resource Priorities specification. What could be helpful is something like the pr attribute in the Resource Hints specification, which is used to indicate the likelihood that a given resource will be used.
With HTTP/2 some of these problems go away, since requests and responses are multiplexed in streams over a single connection. However, the concept of resource priorities remains, with bandwidth apportioned accordingly, and taking various dependencies into account.
Another important concept in HTTP/2 is server push. This involves a server sending a resource to the client before the client has asked for it, making it especially useful for delivering the CSS required to start rendering a page.
Preloading and server push can be used together in HTTP/2 by adding a response header to the root object:
Link: </ images/mypic.jpg>; rel=preload;
Add nopush if you want to use preloading but not server push:
Link: </ images/mypic.jpg>; rel=preload; nopush
Server push is probably more relevant to critical CSS files than it is to imagery, but it’s worth mentioning here because it is possible to adjust the priority given to pushed resources – see Apache’s H2PushPriority directive.
The point to take away
When building any web page, it’s important to consider what visitors are most likely to want to see and do when they land on it. Non-essential scripts can get in the way of important content, and loading them asynchronously doesn’t guarantee that they won’t negatively impact the user experience.