Managing Async Dependencies with JavaScript

4 minute read Updated

Managing asynchronous dependencies with JavaScript can be a nightmare. But there's a better way. It's called Fetch Injection.

I’ve long been inspired by the work of Steve Souders. Back in 2009 Steve published an article titled Loading Scripts Without Blocking, which I first became aware of and studied during my time at Orbitz – where every millisecond a user waited for the page to load had a measurable impact to the business.

Steve’s work was instrumental for performance-critical websites and apps, and even inspired Nicholas C. Zakas to write Loading JavaScript without Blocking the same month Steve’s book Even Faster Web Sites was published.

In his article, Steve covers six techniques for loading scripts without blocking:

  • XHR Eval,
  • XHR Injection,
  • Script in Iframe,
  • Script DOM Element,
  • Script Defer, and;
  • document.write Script Tag.

Web developers unfamiliar with the above techniques are more likely familiar with the async and defer attributes introduced with HTML5, which make it easier to load scripts without blocking the initial page render.

There’s an ugly gotcha, though, when loading scripts asynchronously. It occurs anytime dependent scripts are loaded. Loading asynchronous scripts with dependencies can lead to race conditions.

The problem lingers

To illustrate, let’s look at a popular open source UI framework inching its way toward general public use: Bootstrap 4.

Using Bootstrap 4 will require the following script dependencies:

https://cdn.jsdelivr.net/jquery/3.1.1/jquery.slim.min.js
https://cdn.jsdelivr.net/tether/1.4.0/tether.min.js
https://cdn.jsdelivr.net/bootstrap/4.0.0/js/bootstrap.min.js

Of the three, two are required to load before Bootstrap itself. If these scripts were all loaded asynchronously using the async attribute, the resulting code might look something like this:

<script async src="https://cdn.jsdelivr.net/jquery/3.1.1/jquery.slim.min.js"></script>
<script async src="https://cdn.jsdelivr.net/tether/1.4.0/tether.min.js"></script>
<script async src="https://cdn.jsdelivr.net/bootstrap/4.0.0/js/bootstrap.min.js"></script>

This is where the trouble starts. Given the dependencies, if Bootstrap itself isn’t the last to finish downloading, it will execute immediately when finished – attempting to initialize itself – throw an undefined error and fail to initialize entirely.

What’s worse, the error will happen intermittently due to network conditions, making the cause of the race condition harder to detect.

To mitigate the problem, official Bootstrap prerelease documentation does what other major UI frameworks do, and simply does not promote use of the async attribute. This, in effect, punts the problem to userland and will lead many websites to not one, not two, but three scripts causing blocking behavior and jank in the UI.

Those who’re unaware of Steve’s work may not understand these shortcomings, and will likely copypasta Bootstrap’s code – along with the blocking behavior – directly into their source.

Others may choose to use a bundler to concatenate all of the JavaScript files into one or two packages, and load them asynchronously to avoid these problems, typically leading to a SEO-unfriendly Single-Page Application primed for Front-end SPOF.

What most don’t know though, is there’s a better way. And it doesn’t require any sacrifices to SEO, UX, speed or accessibility.

Introducing Fetch Inject

To help address the problem of loading scripts without blocking I created a JavaScript library called fetch-inject.

Fetch Inject introduces a new technique to the Web – let’s call it Fetch Injection – to dynamically inline assets into the DOM.

Here’s how it fits into Souders’ original script loading techniques:

  • XHR Eval,
  • XHR Injection,
  • Fetch Injection,
  • Script in Iframe,
  • Script DOM Element,
  • Script Defer, and;
  • document.write Script Tag.

Similar to XHR Injection, Fetch Injection loads a remote resource and inlines it into the DOM. Unlike XHR Injection, however, fetch-inject leverages the Fetch API instead of Ajax.

Here’s how fetch-inject can be used to load a script asynchronously and without the blocking behavior previously discussed:

fetchInject(['https://cdn.jsdelivr.net/lodash/4.17.4/lodash.min.js'])

As you may have noticed, the fetchInject method takes an Array argument, allowing it to make parallel asynchronous downloads:

1fetchInject([
2  'https://cdn.jsdelivr.net/lodash/4.17.4/lodash.min.js',
3  'https://cdn.jsdelivr.net/momentjs/2.17.1/moment.min.js'
4])

But why stop at scripts? Why not use it inject stylesheets too:

1fetchInject([
2  'https://cdn.jsdelivr.net/lodash/4.17.4/lodash.min.js',
3  'https://cdn.jsdelivr.net/momentjs/2.17.1/moment.min.js',
4  '//cdn.jsdelivr.net/fontawesome/4.7.0/css/font-awesome.min.css'
5])

And because it uses fetch under the hood, it’s possible to couple async and synchronous code using then:

1fetchInject([
2  'https://cdn.jsdelivr.net/lodash/4.17.4/lodash.min.js',
3  'https://cdn.jsdelivr.net/momentjs/2.17.1/moment.min.js'
4]).then(() => {
5  console.log(
6    `${_.capitalize(moment().endOf('year').fromNow())} we'll be heroes.`
7  )
8})

Which, in turn, enables async dependency management for loading JavaScript-driven UIs like Bootstrap 4 asynchronously and in parallel (including font icons):

1fetchInject([
2  'https://npmcdn.com/bootstrap@4.0.0-alpha.5/dist/js/bootstrap.min.js'
3], fetchInject([
4  'https://cdn.jsdelivr.net/jquery/3.1.1/jquery.slim.min.js',
5  'https://npmcdn.com/tether@1.2.4/dist/js/tether.min.js',
6  'https://cdn.jsdelivr.net/fontawesome/4.7.0/css/font-awesome.min.css'
7])

The fetch-inject library itself makes the Fetch Injection technique available to everyone via an easy-to-use API. It has zero dependencies, weighs about 551 bytes minified and compressed, and comes at you Zlib-licensed.

To get started, install the library from NPM with npm i fetch-inject. Please see the Fetch Inject Project README for additional installation options and more detail.

Looking back towards the future

It’s been a long-time coming, but things are starting to look up for the future of the Web thanks to ES6 and the Fetch API. And with HTTP/2 and ES6 modules right around the corner, expect to see more performant websites worldwide, available on more devices at connection speeds never before possible.

We’ve come a long way since Steve published Even Faster Websites, and there’s still a long way to go to make the Web accessible to everyone. But an accessible Web should be our primary goal. And new techniques like Fetch Injection will help get us there.