roughly in half.
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.
Below are some numbers comparing the LAZY version of the client with the previously released version FRANK.
|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.
Following are remarks of a more technical nature on the implementation of Lazy Loading.
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.
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.
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.
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:
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.