The AMD format for JavaScript modules was created to allow devs to write elegant, modular code rather than the silly global, name-spaced hackfest we’ve been writing. However, AMD has a hidden benefit: it allows modules to be downloaded in parallel, rather than sequentially. This is a potentially huge performance win.
However, does this really matter for production code?
Blocking before Loading
Virtually all of the examples of AMD code out on the web look something like this:
<script src="some-amd-loader.js"></script> <script> someAmdLoader(["moduleA", "moduleB", "domReady!"], function (A, B) { var app = new A(), boot = new B(app); boot.init(); }); </script> |
In this example, we’re loading moduleA and moduleB in parallel. This is the fast way to do it, right? Yes, but also note that we’re loading some-amd-loader.js in a blocking fashion. The second script element (and the rest of the document) has to wait for some-amd-loader.js to finish loading and executing before proceeding.
This isn’t ideal, of course. So how do we fix this?
Answer: by only including one script element in the page and ensuring that that single script allows the page to continue parsing and rendering.
Native async loading
Here’s what we really want:
<script src="myapp-bootstrap.js" defer async="true"></script> |
Newer browsers support the async attribute. async="true"
instructs the browser that it can proceed to parse and evaluate any future script elements without waiting. You’d think a browser would notice that there’s only one script element, right? Just in case, we’re going to tell it how we want it to behave. 🙂
Older browsers don’t support the async attribute. However, they do support the defer attribute. This attribute tells the browser that it may defer execution of the script until a convenient time — even after the body is rendered. Unfortunately, browsers don’t all act the same with this attribute. Most browsers, though, do a fairly good job when there’s only one script element in the document.
Since the browser isn’t waiting for the script’s javascript to load or execute, the script element can be in the head where it belongs. There is no need to put it at the ned of the body element.
So, what’s myapp-bootstrap.js look like in this scenario? The sky’s the limit, but you can imagine a minimal set of requirements:
1. it contains an AMD loader
2. it contains one of the following:
a. instructions to load the rest of the app
b. the actual code for the rest of the app
If you’re compiling/optimizing your AMD modules into a single js file already, it’s not too much harder to concatenate the actual AMD loader onto the front of the file (using requirements 1 and 2b above). Some compilers/optimizers have support for this built in, RequireJS, for example. RequireJS also has a companion loader, almond, that is tiny and can handle the simpler environment inside a compiled file.
So, essentially, all that AMD provided for us in this single-script scenario is a way to ensure ordering of our modules into a single file, it seems. Does this mean that the whole async loading behavior we’ve been touting about AMD is just a bunch of “snake oil“? Not at all. Keep reading:
We ain’t oiling no snakes
Let’s say our web app has several pages. Each of these pages has a core set of modules it requires to function and a set of modules that are unique to it. This is not an unlikely scenario, and is quite likely the most common case.
In this scenario, it could be ideal to allow the common core modules to be fetched from cache and the unique, page-specific modules to be loaded separately. Each subsequent page load would be fast because it’s not loading as much code. For this scenario, requirements 1 and 2a are the best choice. The code inside the single file would look a lot like the original code snippet above:
// loader source code goes here // initialize our app in two parts! someAmdLoader(["moduleA", "moduleB", "domReady!"], function (A, B) { var app = new A(), boot = new B(app); boot.init(); }); |
How finely grained you break up your app is totally unique to your app and your target browsers. If you are targeting older IE’s, then your app is burdened by limited HTTP requests. A small number of fetches per page is best.
More sophisticated apps or ones that can ignore older browsers may be able to take advantage of multiple parallel downloads. However, you must remember that each image, css file, and xhr consumes an HTTP request!
Most of the apps I write have a very sparse body because the UI is built by templates embedded in the javascript. The css is embedded, too (via curl.js‘s css! plugin and the cram optimization tool). This means we can break the app up into several files. Some of these files are loaded when the page loads. Others are loaded when needed (“lazily”).
Do some experimentation to figure out what your ideal situation is. If you already have, please share it with the rest of us in the comments section! There are other ways we could load modules since AMD is very flexible. Do you have an entirely different approach?
Thanks!
— John
[update 2011-09-22 12:35 PM EDT] @getify says that he doesn’t recommend the “defer” attribute due to jQuery/FF3.x issues. see twitter convo here. Also: “defer” isn’t always what you want. Use wisely. 🙂
I also recommend keeping a single script tag on the HTML and deciding what to load afterwards (from inside an AMD module), I wrote two blog posts about this subject:
http://blog.millermedeiros.com/2011/05/single-entry-point-ftw/
http://blog.millermedeiros.com/2011/09/single-entry-point-redux/
it’s not only faster but also **more organized** and **easier to update/scale**…
Hey Miller!
There are some very good points in that first article. Although it’s not a problem for me currently, I definitely appreciate this problem on previous projects: “If something needs to be changed, added or removed, you don’t need to ask the back-end developer to update the files…”
Thanks for posting some working code in your second article. Good stuff!
– J
Pingback: Latest News Updates for Web Developers and Designers #1 | Multy Shades