- 6.1 General Practices
- 6.2 A Multitude of Simple Interfaces
- 6.3 Dense, Rich Interfaces
6.3 Dense, Rich Interfaces
Interfaces requiring large amounts of functionality cannot scale using modular loading, especially when they are used with a client-side application. Even in high-bandwidth, low-latency environments, the time required for each additional request for functionality will turn the application sluggish. Each user action hitting a yet-unloaded piece of the function library will effectively require a synchronous call to the server in order to respond directly to the new action.
6.3.1 Monolithic Applications
Having a monolithic application does not forbid the application from having an object-oriented design, but it does mean that the application loads all at once, preferably in only a couple of files. Because browsers will load only a couple of resources at a time from the same domain, an interface requiring dozens of externally loaded JavaScript files (not even taking stylesheets and images into account) will take a ludicrous amount of time to load even over a fast connection.
While modular applications can take advantage of the caching of multiple files over multiple requests, monolithic Ajax-based applications tend not to have more than one or two initial page loads. They resort instead to having most (if not all) of the application loaded into the browser’s memory at once. By doing this, even incredibly complex applications can respond quickly to the user and support a wide range of functionality on demand.
To keep a monolithic application scalable, developers need to have naming conventions in place to reduce the risk of collisions, which can cause problems in JavaScript without making it obvious that the problems stemmed from a collision in the first place. In the following example, two different pieces of the same application need to define their own Player class. The first class defines a Player as a class that runs a slide show, while the second class defines it as the user of the application:
function Player(slides) { this.slides = slides; } Player.prototype = { slides : [], current : -1, next : function() { if (this.slides[this.current + 1]) { if (this.current > -1) { this.slides[this.current].style.display = "none"; } this.slides[++this.current].style.display = "block"; return true; } else { return false; } } }; function Player() { } Player.prototype = { alias : "", level : 1, login : function(login, password) { var req = request_manager.createAjaxRequest(); req.post.login = login; req.post.password = password; req.addEventListener( "load", [Player.prototype.loggedIn, this] ); req.open("POST", "login.php"); req.send(); }, loggedIn : function(e) { var response = e.request.responseXML; if (user = response.getElementsByTagName("user")[0]) { var alias_node = user.getElementsByTagName("alias"); this.alias = alias_node.firstChild.nodeValue; var level_node = user.getElementsByTagName("level"); this.level = level_node.firstChild.nodeValue; } } };
While PHP throws a fatal error when you attempt to define an existing class, JavaScript will quietly let it happen, overwriting any existing variables and methods, and altering the behavior of existing instances when modifying the prototype. Luckily, JavaScript’s prototype-based object model makes it easy to implement something close to namespacing. By encapsulating each class definition in another object, one that can hold the class definitions for everything within a set of functionality, the classes can exist almost untouched from the previous definition:
var Slideshow = { Player : function(slides) { this.slides = slides; } } Slideshow.Player.prototype = { slides : [], current : -1, next : function() { if (this.slides[this.current + 1]) { if (this.current > -1) { this.slides[this.current].style.display = "none"; } this.slides[++this.current].style.display = "block"; return true; } else { return false; } } }; var Game = { Player : function() { } } Game.Player.prototype = { alias : "", level : 1, login : function(login, password) { var req = request_manager.createAjaxRequest(); req.post.login = login; req.post.password = password; req.addEventListener( "load", [Player.prototype.loggedIn, this] ); req.open("POST", "login.php"); req.send(); }, loggedIn : function(e) { var response = e.request.responseXML; if (user = response.getElementsByTagName("user")[0]) { var alias_node = user.getElementsByTagName("alias"); this.alias = alias_node.firstChild.nodeValue; var level_node = user.getElementsByTagName("level"); this.level = level_node.firstChild.nodeValue; } } };
Now, code using each of the classes can reference either one (without any doubt of which class it is using) by calling new Slideshow.Player() to instantiate a new Player that will display a slideshow. To instantiate a new Player representing the user, the code can call new Game.Player(). By using techniques like this to emulate namespaces, multiple developers can work on large, monolithic applications without fear of class or function name collisions; this practice makes such applications much easier to maintain (see Appendix B, “OpenAjax”).
6.3.2 Preloading
An interface loading just six JavaScript files totaling 40k will load in an average of 150 milliseconds on a LAN connection. This instance does not take long, but the setup will not scale. The loading time grows linearly as the application loads more files, taking double the time for double the files. However, random network fluctuations can cause a higher incidence of bandwidth and latency issues tripping up the loading process, causing it to sporadically take a second or two for a single file.
Even though an application may have its functionality existing in separate JavaScript files, it still can take advantage of the faster load time of a smaller number of files by using the server-side application to consolidate files. This keeps the client-side application maintainable without affecting how the browser will load the scripts necessary for a given interface; this practice supports the monolithic application-loading scenarios with modular application development.
In order to get around this problem, the application can consolidate the files into a single file, requiring only one request to get the functionality of several files. The following example takes two arguments and implements a consolidation of the list of files specified in the first argument, saving the result to the path specified in the second. This static method exists in a generic, globally accessible Utilities class of the application:
/** * Consolidates files into a single file as a cache */ public static function consolidate($files, $cache) { $lastupdated = file_exists($cache) ? filemtime($cache) : 0; $generate = false; foreach ($files as $file) { // Just stop of missing a source file if (!file_exists($file)) { return false; } if ($u = filemtime($file) > $lastupdated) { $generate = true; break; } } // Files changed since the last cache modification if ($generate) { $temp = tempnam('/tmp', 'cache'); $temph = fopen($temp, 'w'); // Now write each of the files to it foreach ($files as $file) { $fileh = fopen($file); while (!feof($fileh)) { fwrite($temph, fgets($fileh)); } fclose($fileh); } fclose($temph); rename($temp, $cache); } return true; }
When using this script on the first load with the same six files, the full script loads in 45 milliseconds on the first hit and in an average of about 35 milliseconds from then onward. When using this script on the first load with the full twelve files, the full script loads in 80 milliseconds on the first hit and in an average of about 50 milliseconds from then onward.
This method can have other functionality built into it in order to make the page load even faster, especially for rather large applications. Once it consolidates the files into a single, cached file, it then can create a secondary, gzipped file by running copy('compress.zlib://' . $temp, $cache . '.gz') so that the browser can load an even smaller file. It even can run the script through a tool to condense the script itself by removing comments and white space and by shrinking the contents of the script prior to gzipping.
By using these methods, even megabytes of script necessary for a rich, full interface can load quickly. The expanses of functionality will add more to the application without dragging down its performance and becoming unwieldy.