How loading JavaScript can slow websites down (even if it’s asynchronous)

Where would websites be without JavaScript? The preeminent programming language for the web has helped turn the Internet from a sea of static documentation into a rich, interactive experience.

But have we come to rely on it too much?

Blocking scripts

It’s not uncommon for a modern web page to contain references to a dozen or more JavaScript files, all adding features and functionality designed to enhance the user experience.

However, some years ago we started to realise that scripts could also be a menace. Browsers would wait for external JavaScript to load and execute before continuing to build the web page. And if a script were delayed for some reason, visitors could be left staring at a blank page while they waited.

The simplest way around this was to put scripts at the bottom of the page. This meant that a slow-loading script wouldn’t block the rendering of content above it in the HTML. Another alternative was to use inline JavaScript to add a reference to an external JavaScript file. This allowed the external file to load without blocking the page’s construction.

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.

This hasn’t eliminated the issue of blocking scripts. If JavaScript is used for feature detection, for example, or to add content dynamically, it may still have to be loaded synchronously, at the top of the page.

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’

When someone visits a web page, they’re usually interested in the content. Although JavaScript gives us the opportunity to deliver a whole range of enhancements, people are still often primarily interested in consuming information.

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.

The result can be that images are effectively delayed by scripts. We’re back to blocking JavaScript.

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.

net1

It’s also helpful to look at the connection view waterfall, which shows script files tying up connections ahead of images:

net1connectionview

Is there anything we can do about this?

Domain sharding

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.

Adding scripts using inline JavaScript

Of course, we don’t have to include script references directly in the HTML. We can use some inline JavaScript to add <script> elements to the page. If we do this after we’ve referenced our images, those images should start loading before the scripts.

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.

Preloading images

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:

net2

And the connection view:

net2connectionview

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.

HTTP/2

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.

Leave a Reply

Your email address will not be published. Required fields are marked *