Roswell Studios

139 Fulton Street, Ste 132
New York, NY 10038

Guide to asynchronous Shopify sites

— November 12, 2018

Shopify has a great content distribution network (CDN). It can easily load a complicated page with many images in 0.2-0.3 seconds. However, it takes some time to load and execute the Javascript that makes the site run. Worse, it might take 4 seconds or more to load all the poorly written plugins and user tracking scripts. Mega-worse: some random script can stop the site from loading at all. How to get the page loading in 0.3 seconds, present that, then load the JS before a human can react and click on things?

Normally, you would load and execute JS in order:

  • head
  • jquery.com
  • jquery1.com plugin
  • jquery2.com plugin
  • 1tracking.com script
  • 2tracking.com script
  • 3tracking.com script
  • asset script
  • asset script
  • asset script
  • body
  • page script with {{liquid}}
  • page script with {{liquid}}
  • page script with {{liquid}}

The problem with this is only the asset script is on Shopify’s CDN, so by the time it gets down to the body, the browser has already had to stop, connect to 7 different hosts, load and execute that code, then continue.

One easy win is to make all the tracking scripts async. This is probably already done. That’s how easy it is.

The next easy win is to combine all other the scripts to vendor.js (3rd party stuff that will rarely if ever change) and theme.js (library code that makes the theme run), or just one theme.js with everything (assuming you don’t need to debug it, and that future developers have access the source files). Ideally, use the slate commands to combine and minimize the files. Less ideally, just paste it together. Now all the important stuff is coming off Shopify’s CDN, making fewer DNS lookups and calls to random servers.

The next win is to make those async as well. This presents problems:

  • head
  • vendor.js async
  • 1tracking.com script async
  • 2tracking.com script async
  • 3tracking.com script async
  • theme.js async
  • body
  • page script with {{liquid}}
  • page script with {{liquid}}
  • page script with {{liquid}}

When it tries to execute the page scripts, the browser has only started loading vendor and theme. The solution to this, which you may recognize from things like Google Tag Manager’s dataLayer, is to create a basic framework, add things to the framework, then execute it once everything loads.

  • head
  • var pReady = [], $ = function(f) {pReady.push(f)}
  • vendor.js async
  • 1tracking.com script async
  • 2tracking.com script async
  • 3tracking.com script async
  • theme.js async
  • body
  • $(function(){ page script with {{liquid}} })
  • $(function(){ page script with {{liquid}} })
  • $(function(){ page script with {{liquid}} })

The body scripts always should have been wrapped in some sort of document.onready code, anyway. jQuery’s $(function) thing is the shortest method of that. The trick is that the $ the page scripts are using isn’t the real jQuery $: all it does is take and save functions for later use. If you pass it anything else, it gives you an error telling you missed the $(function(){}) bit. The last thing vendor.js does is execute all those functions via the real jQuery $(). You’ll note theme and vendor load independently. Theme needs to use $(), too, or else only set things up in its own basic framework object, and allow the page specific code to init things with the various theme and section settings. Theme.js should most definitely not do anything that triggers a page redraw. Neither should vendor, but some jQuery related stuff will. Avoid as much as possible.

There’s still another step: all the tracking scripts and poorly written plugins, even if async, can execute and block the page loading before the real js runs. It would be best to order it such that the real work happens first.

  • head
  • var pReady = [], $ = function(f) {pReady.push(f)}, theme = {};
  • vendor.js async
  • theme.js async
  • body
  • $(function(){ theme.function({option: {{liquid}}} }) })
  • $(function(){ theme.function({option: {{liquid}}} }) })
  • $(function(){ page script with {{liquid}} })
  • footer
  • $(function(){ tracking script loader(1,2,3) })

Because the $ functions are executed in order, it starts loading vendor, theme, sees and stores (but does not execute) the $ functions. At some point in time, while the customer is looking at a page with css and images, the document.ready runs, executing the page code, then finally starting the tracking load. While that is happening, the customer is looking at and able to interact with the page. It may be missing Shopify apps that take some time to load from remote databases (reviews, wishlists). Ideally, the design should not put these things at the top of the page. By the time a human being has scrolled down, the app will have had perhaps seconds to have loaded that. There may be a point where that new JS locks up the browser with excessive screen redraws, which is a poor user experience, but at least the user is looking at something. Terrible code would have locked up the browser anyway, with the user looking at nothing. Ideally, don’t use that app.

Say you have a single page with custom requirements, such as needing another jQuery plugin. It is a large plugin, only used this once, and you’d rather not add it into vendor.js. Ideally, it would be able to pick up things on its own (div data-slider=”options”), have its own dataLayer-style setup object (anything that starts “x = x || {};” might), or failing that, just wait for it to load:

  • head
  • var pReady = [], $ = function(f) {pReady.push(f)}, theme = {};
  • vendor.js async
  • theme.js async
  • body
  • $(function(){ theme.function({option: {{liquid}}} }) })
  • $(function(){ theme.function({option: {{liquid}}} }) })
  • largePlugin.js async
  • $(function(){ function run() {$(‘#stuff’).plugin(things);} function load() { if (typeof $.fn.plugin == ‘function’) { run(); } else {setTimeout(load, 30); } } load(); })
  • footer
  • $(function(){ tracking script loader(1,2,3) })

There’s also deferred, which is nice in that it preserves the order of scripts loaded with deferred, but still has problems as script-tags in the page are not defer-able. So you’ll have problems with running $(function(){}) before jQuery loads. Async uses the same fixes and has some speed advantages, so use that.

If the browser does not support async, then everything loads in order, which, as it loads the tracking junk last, is still the best experience that browser can do.

Back to all