7 April 2011 Update: My friend and former colleague Richard Backhouse has written an excellent companion to this blog entry talking about how he actually implemented many of these patterns in Jazz and Zazl.
Warning: The following blog entry will definitely be unintelligible to readers who are not software developers.
It may possibly be unintelligible to readers who are software developers. 🙂
Motivation
Between mid-2005 and the end of 2009, I worked in IBM Rational on the software infrastructure for the Rational Jazz browser-based user interfaces, hence “web UIs”. For various reasons I decided that we should provide an “extreme Ajax” architecture [1] which required us to have to load a large amount of JavaScript and CSS. Since people don’t like UIs that load slowly, we [2] spent a lot of time exploring patterns and techniques that would cause the JavaScript and CSS code to load as quickly as possible.
Recently an IBM Software Group Architecture Board workgroup asked me to document some of these techniques, and based on the positive response my internal write-up received, I thought I would tweak the write-up and publish it externally.
Preamble: Modular Software Development
In the “Motivation” section I mentioned that the design decision to build “Extreme Ajax” UIs led to a technical problem of needing to load a large amount of JavaScript and CSS code quickly. Users of course don’t care how you load code, they just care that the UI loads quickly. Theoretically you could solve the code loading problem by coding a single large JavaScript file and a single large CSS file, but of course developing in this way would eliminate all of the benefits of modular software development, which have been discussed in depth elsewhere [3].
The only reason I mention this point is that the practice of developing modular Ajax software complicates the task of making the code load quickly, as the following sections will show.
Overview of Optimization Techniques
Fundamentally, there are only a small number of things you can do to make Ajax code load faster:
- Deliver Less Code
- Concatenate a Large Number of Files into a Smaller Number of Files
- Compress Code Content
- Cache Code Content
The following sections address each of these techniques in some detail.
Deliver Less Code
Though it may sound trite, the simplest way to improve code loading performance is to deliver less code. This can be accomplished in two basic ways:
- Design simpler web applications that require less code
- Do not load code until it is needed by the UI
It’s outside the scope of this topic to discuss the pros and cons of simpler vs. more complicated web UIs, so I won’t say much about 1. other than to re-observe that speed is a feature and that little bits (or large bits) of functionality tend to add up and make a web page slower to load.
The second topic is a bit more mechanical and thus fits better into this blog entry. In a nutshell, the code required to power a particular UI is often much smaller than the total product code base, especially for feature-rich products like Rational Team Concert [4]. A common optimization technique in any system that has this characteristic [5] is to defer loading code until you know you need it. This technique is usually referred to as “lazy loading”.
One implements lazy loading by first understanding the relationship between a UI part (e.g. a web page, a dashboard widget, an Eclipse view, etc.) and the code required for that part to run. This requires that your programming language or framework have some notion of modules and module dependencies. Although the JavaScript language proper has no such construct, in IBM we use a JavaScript toolkit called Dojo that provides a module construct. Basically each JavaScript file can declare a module name (like “com.ibm.team.MyUIPart”) and also can declare dependencies on a number of other modules (like “com.ibm.team.MyUIPartController”). The set of all modules and their dependencies allow you to build an internal representation of the modules’Â dependency graph [6]. Once you have the dependency graph, you only need a mechanism for defining UI parts and their top-level dependencies and then you can quickly and easily walk the dependency graph calculate the complete set of modules required to execute the UI part.
For a fuller explanation of computing JavaScript and CSS dependency graphs, see Appendix B below.
There is some complexity involved with the dependency calculations to implement lazy loading, so it’s only worth pursuing if you can defer loading a large amount of unnecessary code. This was true in Rational Team Concert where I estimate a given UI (like the bug submission form) probably contains less than 5% of the total code base.
The simplest lazy loading approach is to consider the web page itself the “UI part” and thus load all of the JavaScript code that the page needs as a simple <script> tag when the page loads. However sometimes it’s necessary to load additional code later in a page’s lifecycle. This leads to some more advanced lazy loading techniques.
Consider a “dashboard UI” that aggregates a bunch of little UI widgets in a single web page. Using the simple lazy loading approach described above, we could calculate the total code needed by the dashboard by considering the dashboard framework and the set of all dashboard widgets to be the UI parts, and unioning the JavaScript modules transitively required by each of these.
However a common feature of a dashboard is to allow a user to add new dashboard widgets to the page. If we have a large number of possible widgets we probably don’t want to load these whenever we load the dashboard, especially when you consider that probably 95% of the time the user will not modify the dashboard. So how do we lazily load the code for the new dashboard widget? Well, as you’ve probably guessed, it’s just a matter of performing set subtraction between the currently loaded set of modules and the set required by the new dashboard widget, then loading the difference (i.e. the modules that are needed but not yet loaded).
Although this deferred lazy loading technique is easy to describe, it’s a bit tricky to implement, so I recommend you stick with the single file loaded as part of the top-level page. But this of course begs the question “How to go from a set of fine-grained modules to a single monolithic file?” This is the topic of the next section.
Concatenate a Large Number of Files into a Smaller Number of Files
Each JavaScript file or CSS file that is loaded requires its own HTTP request and load processing by the browser. Therefore each load of a JavaScript or CSS file introduces a non-trivial amount of latency, bandwidth, and local CPU overhead that delays the user from actually using the web application. A common technique for reducing this sort of overhead is to batch – that is turn a large set of small resources into a small set of large resources. As with the “lazy loading” technique, the most common approach to concatenate files is to first determine the subset of all files needed, and then to append them one after another after another into a single file. Though concatenation is conceptually simple, there are several design considerations.
The first consideration is “What should be the granularity of the concatenated file?” An extreme answer to this question is “A single file containing only the modules required by the user’s immediate UI”. This is the approach that the Jazz Web UI framework takes. Another approach would be “Several logical layers of file sets, in the hopes that the layers can be reused by multiple UIs and therefore benefit from cache hits”. This is the approach taken by the Dojo build system. It is hard to judge the pros and cons since the efficacy of each depends on other factors like the nature of the UI, the nature of the layers, and the usage of layers across different UIs.
A second consideration is concatenation order. This is important because one module might require the presence of another module to function correctly. This can be easily solved by concatenating (and therefore loading) modules in reverse dependency order. I.e. the module that depends on nothing but is depended upon by everything loads first while the module that depends on everything but is depended upon by nothing loads last. This is also important in a web UI that uses Dojo since the Dojo module manager will make expensive synchronous XHR requests if it determines that a module requires another module that is not yet loaded. If you load your modules in reverse dependency order, each dojo.require statement becomes a noop.
Reducing the number of module HTTP requests via concatenation will reduce the bandwidth overhead incurred by many HTTP requests, it does not help with the bandwidth required to load a very large file. However, it is possible to significantly reduce the raw bytes of required code by compressing the code content. I cover this in the next section.
Compress Code Content
Imagine that you’ve taken 50 JavaScript files and 40 CSS files and concatenated them down to a single JavaScript file and a single CSS file. Each of these files may still be huge because of raw code size plus the size of whitespace and comments. There are two ways to makes these files smaller:
- Gzipping
- Minification (JavaScript only)
Enabling gzip when serving JavaScript and CSS probably provides the best return on investment of any of these techniques. In Jazz we often see files get 90% smaller simply by running them through Gzip. When you’re delivering hundreds of kilobytes of code, this can make a large difference. There is some overhead required to zip on the server-side and unzip on the client-side, but this is relatively cheap vs. the bandwidth benefits of the Gzip compression. Finally, Gzipping has the nice characteristic that it is a simple transformation that has no impact on code content, as observed by the ultimate recipient (the browser runtime).
Minification is the process of stripping unnecessary code content (e.g. whitespace and comments) and renaming variables to shorter names in a self-consistent way. Unlike Gzipping, minification is a unidirectional transformation (you never “unminify”). The following code demonstrates minification.
// A simple adder function
function add(firstNumber, secondNumber) {
   var sum = firstNumber + secondNumber;
   return sum;
}
… might be transformed into …
function add(_a, _b) { var _c = _a + _b; return _c; }
Note that you can only minify tokens that meet the following criteria:
- Their semantics don’t change when you rename them. By this rule you cannot rename ‘function’ since it is a JavaScript programming language keyword.
- You can consistently rename all instances of the token across the entire set of loaded code. Because of this it’s dangerous to rename API names like ‘dojo’ or CSS class names since it is hard to find all references to these names. Therefore usually only local variables are renamed, but this can still yield a non-trivial savings.
Although it should be obvious, it is possible to use both minification and gzipping together, though you must always minify before gzipping.
Cache Code Content
A final high-yield technique to improve code loading performance is to cache responses to reduce either bandwidth used or latency. There are two basic caching techniques [7]:
- Validation-based caching – Where you check each time to see if you have the most recent version of some document, and if you already have the most recent version you load it from your cache rather than fetching the document again.
- Expiration-based caching – Where the document server tells you that a certain document is good to use for some specified period of time. If you need to load the document and its expiration date is later than the current time, you may load the locally cached version of the document without even asking if you have the most recent version.
Obviously expiration-based caching is going to be faster than validation-based caching [8] (because you don’t even have to ask) but it is trickier to implement because you have to know when code is going to change. Consider the following scenario:
BigCo updates its web site every six weeks and therefore sets expiration dates on all of its web code from +6 weeks from time of last deployment, which means that each user only has to load new code once after each deployment. However, two weeks after a certain deployment, BigCo discovers a nasty security bug in its code that forces an unexpected patch deployment. However, any customer that accessed BigCo’s web site since the previous planned deployment does not received the patch because their browsers have been told not to load the new code for another four weeks.
The solution we found to this problem for Jazz was to use validation-based caching on web pages, and expiration-based caching with versioned URLs on JavaScript and CSS files referenced within the web page. Here’s an example:
<html>
<head>
<script type="text/javascript" src="../code.js/en-us/I20101005-1700"></script>
<link rel="stylesheet" type="text/css" href="../code.css/en-us/I20101005-1700" />
</head>
</html>
In this example, the HTML page includes a script and CSS reference to files with versioned URLs where the version ID corresponds to the build in which the code was last change. Each code request responds with an expiration header of plus one year. If the code were updated, the value of the URLs would change which would prompt the browser to fetch the new file which again will contain an expires header of plus one year. By driving the state off of the URL rather than a last-modified header, we are able to use expiration-based caching safely and since in our Jazz applications the size of JavaScript and CSS are much larger than the size of the HTML pages, the vast majority of our code will be loaded from the user’s disk on subsequent visits to a web UI.
You can also take advantage of caching with CSS references to images. Basically you can rewrite any ‘background’ image URL to use a similar versioned URL and a long expires header. This can be an especially big win since some images get loaded many times.
Appendix A: Debug-ability
A related issue to optimization is the ability to debug the web application. Several techniques above (concatenation, minification) change the code content vs. how it appears in a developer’s workspace. In fact, raw optimized code is effectively impossible to debug because of its inscrutability. The basic solution to this problem is to enable a “debug mode” where the code is loaded in a less optimized form so that the code matches what the developer sees in his or her development workspace. In the Jazz Web UIs, we’ve made debug-ability a first class concern since day one, and you can enable it simply by appending ?debug=true to any web page. The page may load significantly slower, but this is tolerable since you only experience this when you explicitly ask for it (i.e. a user would never see it) and since you would otherwise not be able to debug the code.
Appendix B: Determining Dependency Graphs in JavaScript and CSS
Several of the techniques above (lazy loading and concatenation) depend on understanding the dependency relationships across all sets of code. This section describes the basic mechanics of building the dependency graph for JavaScript and CSS.
The abstract solution for any dependency graph is straightforward: determine what each module depends on via inline dependency statements or external dependency definitions and then compute the union of each module’s dependencies to build the dependency graph.
It is straightforward to compute a JavaScript dependency graph with Dojo. Simply treat each dojo.require statement as defining a unidirectional dependency relationship between the module containing the dojo.require statement, and the module referenced by the dojo.require statement. Each of these “A depends on B” dependency statements is an arc in the overall dependency graph, so once you’ve analyzed all of the Dojo-based modules, you have your JavaScript dependency graph. Obviously a similar technique could be used with non-Dojo modules either via comparable inline statements (foo.dependsOn(“bar”)) or via external dependency definitions (foo.js depends on bar.js).
Once you have the JavaScript dependency graph, you simply need to know the root nodes needed by any particular UI. Consider the following simple dependency graph (the arrow direction indicates the “depends on” relationship).
A -> B
B -> C
B -> D
C -> E
If a UI knows that it has a root dependency on module C (“Jazz Work Item Editor”), then it needs only load C and E. However, if a UI depends on module A (“Team Concert Web UI”),t hen it needs to load all of the modules. This introduces a secondary dependency relationship; the dependency relationship between some logical notion of “a UI” and “the set of top-level JavaScript modules required to run the UI”. In Jazz we express this relationship via server-side Eclipse extension points (e.g. “the page at /work-items depends on the module com.ibm.team.WorkItem.js and all of the modules depended upon by com.ibm.team.WorkItem.js”), however the relationship could be specified in any number of ways.
There is no common solution to build a CSS dependency graph, even with Dojo. Theoretically you could just externally define a bunch of dependencies between CSS files.
A.css -> B.css
B.css -> C.css
… and then use a technique similar to the one described above where each logical UI describes its dependency on top-level CSS module (or modules) and then you simply walk the dependency graph for the top level module (or modules) until you have all of the CSS.
In Jazz we took a slightly different approach. Rather than declaring CSS to CSS dependencies, we declare JavaScript to CSS dependencies and then derive the CSS dependency graph from the JavaScript dependency graph:
A.js -> B.js
A.js -> A.css
B.js -> B.css
Using this example we know that whenever a UI requires A.js, then it transitively requires the JavaScript A.js and B.js and the CSS A.css and B.css. The reason why we chose this approach is because it allowed us to only surface JavaScript as a top-level API and CSS dependencies can remain an internal implementation detail and therefore change without breaking anyone. For instance in the example above we can imagine B.js is a shared library delivered by team B and A.js is an application delivered by team A. When A gets loaded, all of the JavaScript and CSS provided by both teams A and B will be loaded, but team A need not care about the CSS delivered by team B.
A final issue of course is circular dependencies. Basically you have to decide how tolerant your system will be of circular dependencies and how it will try to recover from them. In Jazz I believe we try to tolerate them but loudly complain about them via WARN-level log messages so people usually eliminate them pretty quickly. In my view, a circular dependency is either a symptom of poor design, an implemnetation bug, or both.
Footnotes
[1] In hindsight I believe we went a bit too far with the “extreme Ajax” approach, but that’s for another blog entry.
[2] When I say “we” in this article I basically mean myself and Richard Backhouse, who collaborated on the design of the Jazz Web UI code loading infrastructure between 2005 and 2009. Though I was quite involved with the design, Richard implemented everything. Randy Hudson took over this code in 2010 and has added quite a few of his own ideas and improvements.
[3] My favorite writing on modular software development is Clemens Szyperski’s “Component Software, 2nd Ed.”
[4] The very first Ajax code we wrote starting in early 2006 evolved into the Rational Team Concert web UI. Later we factored out a subset of this code to be the Jazz Foundation web UI frameworks and common components which were then used by a number of other Rational products. You can actually see Rational’s self-hosting instance of Rational Team Concert at Jazz.net, though this requires you to first register with Jazz.net (frowny face).
[5] E.g. the Eclipse Platform, from which I learned about lazy loading, and many other design patterns.
[6] It is obviously necessary to avoid circular dependencies between modules.
[7] I’ve written up a longer article on validation-based caching vs. expiration-based caching for anyone interested.
[8] It’s a bit trickier than it should be to make expiration-based caching work consistently across all browsers. The short version of the solution is to use every possible directive you can to tell the browser to use expiration-based caching; e.g. expires, cache-control, etc. Mark Nottingham has a blog entry with some more detail on this topic.