The ZCS web client recently underwent a significant amount of change in order to load Javascript code as needed, rather than loading all of it on initial login. Those changes included the development of a package system, and a lot of shuffling around of client code over the course of about two months. The results have been worth it: the client has seen its initial load time cut
roughly in half.
From the outset, the code for the Ajax client has been well-separated in terms of responsibility (following the model-view-controller paradigm), but poorly separated along the lines of function. Applications knew each other’s details, and classes accessed each other’s properties and functions freely. There is no real privacy control in Javascript, and we weren’t enforcing any. Since the entire pile of client code was being loaded at login, there was no real incentive to maintain boundaries if they slowed development. But that pile had become a mountain and we started to hear back from customers that initial load time was a problem.
When a user logs in, she wants to see her Inbox. But we were making her wait first for the entire client code base to be downloaded and parsed – all sorts of calendar, contacts, notebook, etc code that she may never use, and even worse, she may not even have enabled. Obviously, much of that initial load is a big waste of her time. So we decided to run a simple test: Remove any code from the client that wasn’t needed to display the Inbox, and compare load times. The result, as expected, was dramatic: a 40-50% reduction in initial load time. The “Lazy Loading” project was on.
Ross’s characterization of me at the beginning of the project was apt: standing at the edge of a high dive, staring down at water that looked very cold and very far away. I knew that it would require a fair amount of client re-architecting, and that we would need to find or develop a package system to load Javascript on demand. Since the project would involve a significant rewrite of the client, touching nearly the entire code base, it was also a chance to sweep through and clean up other miscellaneous ugliness. After a couple weeks of thinking and poking at things, it was time to start the rebuilding. I recruited Andy Clark to write the package system while I started tearing apart the client. Once started, the work went pretty fast. Of course, the deeper I got the more I realized there was to do (small pieces still remain). The quiet times during the winter holidays were a good time to crank out code, and the new client was ready for testing in mid-January.
Below are some numbers comparing the LAZY version of the client with the previously released version FRANK.
FRANK | LAZY | |
---|---|---|
Footprint (uncompressed) | 2744K | 1658K |
Footprint (compressed) | 596K | 374K |
Lines of code | 87053 | 54454 |
Footprint is related to download speed, and lines of code is related to parse time. Bear in mind that these measurements were done in late January, so they have likely changed slightly since then.
Below are some average load times in seconds.
FRANK | LAZY | gain | |
---|---|---|---|
IE7 | 8.3 | 5.1 | 39% |
FF2 | 10.6 | 4.7 | 55% |
Following are remarks of a more technical nature on the implementation of Lazy Loading.
Packages
The obvious way to load code on demand is through a package system, since the most efficient way to load code on demand is to gather related code into packages. Loading each individual file as it’s needed is possible, but the network cost of all those requests is prohibitive. The first thing we needed was a package system that could track which packages and files had already been loaded, so that a package is not loaded until the client asks for it, and it’s only loaded once.
There are two common mechanisms for loading JavaScript dynamically: XHR and script tag insertion. With XHR, the contents of a .js file are retrieved as a string and eval’ed. With script tag insertion, a <script> tag with a “src” attribute pointing to the .js file is added to the DOM, and the contents are parsed automatically. XHR has the advantage that it can be done synchronously, which leads to a simpler programming model. The calling code simply blocks until the package has been loaded, then continues with code that depends on files in the package. Script tag insertion leads to an asynchronous programming model because you must listen for an event to tell you when the script has finished loading into the DOM so that the classes it defines are available. Script tag insertion appears to be slightly faster, probably due to the creation and handling of large strings that the XHR method requires. But the difference isn’t significant.
We considered importing the Dojo packaging system, but for several reasons we decided to write our own. Of course, the overriding reason is that we had a good idea of what we wanted, and after a quick look at the Dojo system, we decided it would be easier and quicker to write our own rather than adapt to Dojo’s. We admittedly didn’t take a thorough look at Dojo, so apologies for any mischaracterizations. We wanted our underlying package system to support loading code through either XHR or script tag insertion, and the Dojo package system only does XHR. We also wanted it to be aware of the notion of packages as collections of files, and in Dojo a package appears to be a single file (normally of related functions). Our code is partitioned along object-oriented lines into sets of classes, where a file normally defines a single class, so we have a much greater need to bundle files into packages.
Andy created two new classes to handle package loading. The smaller, AjxLoader, is a barebones wrapper around XHR and does not provide the additional support that the web client uses for SOAP requests (AjxRpc, AjxXmlDoc, etc). AjxPackage does the real work of managing packages: mapping package names to the file system, knowing which packages/files have been loaded, loading packages, and evaluating loaded code when necessary. Sitting on top of AjxPackage is AjxDispatcher, a class I wrote to provide some package-related infrastructure to the client. It provides methods to register and run API methods (which know which packages they depend on), and a mechanism for calling functions before and after a package loads, for example to display and then clear a “Loading…” screen. The client interacts with the package system via AjxDispatcher and doesn’t call AjxPackage methods directly.
Apps
Most of the packages we’ve created map to applications such as Mail, Contacts, or Calendar. Packages can be arbitrarily granular, so we have created specialized packages based on need, such as ContactsCore, which is loaded to parse the background load of contacts that happens on login. It doesn’t contain any of the UI code needed to display contacts, but that isn’t needed yet. Many apps have “*Core” subpackages which provide limited functionality, normally to handle app-related data without having to display it.
During a production build, the files in a package are concatenated together, then the resulting aggregate file is minimized (white space, debug statements, and comments are removed) and compressed. Due to that aggregation, packages are additive so that code is not parsed more than once. For example, the package Contacts does not contain any files that are in ContactsCore. Normally, ContactsCore will be loaded during login, and Contacts only when the user goes to that application. Any file that appears in both packages will be loaded and parsed twice, which is unnecessary. The package system can’t do anything about it due to the aggregation, so we make sure that the Contacts package simply adds the files needed beyond the ContactsCore package. Any client code that needs the contacts application must load both packages.
A similar concept is at work in loading what we consider to be our base packages. Since the user has already visited the login page, and the browser will have cached those files, we separate some of the base packages into what is loaded to display the login page, and what additional code is needed to launch the client. The result is that we have a pair of packages to load our Ajax framework: AjaxLogin and AjaxZCS. Remember, there is no restriction on creating packages to fit your needs.
Debugging
One unfortunate consequence of loading files via XHR is that they are not recognized as Javascript files, and the code is not accessible from within a debugger. Any break hit in that code will show up as being within the package system. The author of Firebug is working on a way to handle that. Until then, the way to pre-load packages via JSP files is to pass package names to the query string argument “packages” (assuming it’s not the package system you’re trying to debug). To load all packages, set the value of that argument to “dev”:
http://localhost:7070/zimbra/mail?debug=1&packages=dev
App Classes
Before the re-organization of the web client to support lazy loading, apps, though divided into subdirectories, were freely available to each other. Even if an app wasn’t enabled, all of its code and constants were available. Now that app code is within a package that may not be loaded, formerly public methods and constants are not available. Each app has been isolated. Still, we need the ability for apps to invoke methods of other apps, and we need to be able to look up their constants. The solution we’ve chosen for this is a registration mechanism – each app class (eg ZmMailApp), which used to do minimal work (mostly return references to its controllers), is now responsible for defining its public interface. It does that by defining a number of public constants and methods, and by overriding some abstract public functions defined in its parent class, ZmApp.
ZmApp provides storage for app-related info, a set of abstract registration functions to be used during app construction, and functionality that is common to all apps. Much code that was formerly app-specific has been converted into generic code that checks constants created during app construction.
In general, an app class takes the following steps to set itself up for lazy loading:
1. Define some needed constants in static code.
2. During client startup, any enabled app will have an instance created through a call to its constructor. The parent constructor in ZmApp calls the following functions, which an app overrides as necessary:
this._defineAPI();
this._registerSettings();
this._registerOperations();
this._registerItems();
this._registerOrganizers();
this._setupSearchToolbar();
this._registerApp();
Any data that is relevant to a single app will be defined by that app. Data that is common to many or all apps (for example, the operation “tag” or the organizer “folder”) is defined in common code. The functions above are wrappers around calls to functions that do the actual setup.
Always refreshing to hear a raoitanl answer.