Faster Page Loads With Modular JavaScript

Any web developer who has written a dynamic web app and then tried to make it fast has encountered the dreaded "Render blocking JavaScript" from Google's PageSpeed Insights. There are a number of techniques to prevent this, but no matter how you shake it every line of JavaScript your web app uses needs to be downloaded, parsed, compiled, and (probably) executed. All of that takes time.

Show Me The Money

What users want is to see and interact with a web app as soon as possible. Achieving the "see" portion is a matter of getting the app's markup ahead of the JavaScript (see Server-generated HTML In a Single-page Web Application). Making apps interactive sooner requires some more thought.

Applications on Stilts

A web app becomes responsive only as soon as all of the necessary JavaScript has been downloaded, parsed, compiled, and has either executed or is ready to execute. What we found at Roadtrippers was that the best way to achieve this was to reduce the amount of JavaScript we load with the initial page load. With a smaller footprint the browser has less to download and less to process, allowing our application to get to its business.

The first step is to determine exactly how much JavaScript is needed to load and run the app's front page. This alone carved over 20 seconds off of the Roadtrippers page load time in many cases.

20 Seconds?!?!

As web developers it is easy for us to excuse slow load times. We simply assume that the problem is one of the following:

Really, none of that matters if a user comes to your app and thinks "This is so slow. This sucks." At Roadtrippers, we used a service called "Peek" by UserTesting to get a video of an actual person trying Roadtrippers for the first time. It was utterly painful to watch this user wait nearly 30 seconds for the app to load, so we decided to do something about it.

Dancing With The One You Came With

When thinking about how to enhance Meridian (Roadtrippers' front-end framework) to support modular JavaScript loading, we needed a solution that would fit well with our Ruby on Rails server. Rails' asset pipeline provides tools that will take a bundle of JavaScript files, minify them into a single file, and provide them with a digested file name. Meridian takes advantage of this functionality.

Deciding what code belonged in which package was difficult. At the time, all of the JavaScript for Roadtrippers was packaged into two minified JavaScript files: application.js and map.js. Over time these files had grown very large and took the browser an exceptionally long time to download, parse and compile. We decided to break the JavaScript into modules that matched the discrete sections of Roadtrippers.

This meant we built individual packages with the code for:

Getting More JavaScript

Having these individual packages is lovely, until it comes time to load them on demand. Before we can execute JavaScript there are a few steps that need to take place:

We built a module into Meridian to handle these steps.

Once More Into the DOM

Before we can execute any JavaScript, we must enlist the browser's assistance in fetching the script and loading it. The first step to this is to create a script element.

var script = document.createElement('script');
script.type = 'text/javascript';
script.src = 'path/to/javascript';
document.body.appendChild(script);

Once Meridian appends the script to the DOM, the browser will fetch the JavaScript file, parse it, and compile it. Once it is done, we know we can start utilizing that code.

Call(back) Me, Maybe?

Before we append that script we should add an event listener so that our app can take action on the newly-loaded JavaScript.

var script = document.createElement('script');
script.type = 'text/javascript';
script.src = 'path/to/javascript';

script.addEventListener('load', function() {
  /* ready code goes here */
  });

document.body.appendChild(script);

Of course, if something goes wrong when loading that JavaScript it would be nice to respond appropriately:

var script = document.createElement('script');
script.type = 'text/javascript';
script.src = 'path/to/javascript';

script.addEventListener('load', function() {
  /* ready code goes here */
  });

script.addEventListener('error', function() {
  /* error code goes here */
  });

document.body.appendChild(script);

Within Meridian we utilize promises, specifically when.js from CujoJS. When loading packages we separate the load and error behaviors from the actual package loading:

_when.promise(function(resolve, reject) {
  var script = document.createElement('script'),
    onLoad, onError, unbind;

  script.type = 'text/javascript';
  script.src = 'path/to/javascript';

  unbind = function() {
    script.removeEventListener('load', onLoad);
    script.removeEventListener('error', onError);
  };

  onLoad = function() {
    unbind();
    resolve();
  };

  onError = function() {
    unbind();
    reject();
  };

  script.addEventListener('load', onLoad);
  script.addEventListener('error', onError);

  document.body.appendChild(script);
});

Notice that after script has been resolved, we clean up our event listeners. As they say, "Your Mom Doesn't Work Here."

Kickstart My Package

In Meridian, we established a convention that each package would have an initialize method to handle. This method gets called as part of the sequence of actions encapsulated within the resolve call above, before any application-defined post-load code.

This convention allows us to do the following sort of thing:

rt.loader.ensureLoaded('my_package').then(function() {
  /* start using code from my_package */
});

Because Meridian's package loader has initialized everything, we can trust that it is safe to use that code. This sounds obvious, but is a massive improvement over relying on capricious events to execute application code.

Playing Nice Together

I mentioned Rails' asset pipeline earlier. In order to keep our application versions consistent, we rely on digested assets. When a JavaScript package is updated, it is assigned a new digest. Meridian gets a list of the digested package names when it loads up, so that it retrieves consistent versions of the packages. This keeps the user from getting weird combinations of old and new code.

Meridian relies on the Rails server to provide it with the list of packages at initialization. The Rails server can do this because it knows the digested names. This allows us to build a mapping of the application-defined names to the actual JavaScript file names. Meridian takes this mapping and keeps track of which packages have been loaded. This way Meridian only fetches each package once, regardless of how many times ensureLoaded is called.

Today when Roadtrippers loads, it fetches only the JavaScript it needs to do the following:

Should the user land on an end point other than the main welcome page Meridian will load the necessary JavaScript to load the interactions on that page as well. This allows our users to start planning their trips rather than waiting for code that they are never going to execute.

Software architect at Roadtrippers, living in and loving Cincinnati with his family.