Chapter 9. Web Patterns

The rise of Node.js has proven that JavaScript has a place on web servers, even very high throughput servers. There is no denying that JavaScript's pedigree remains in the browser for client side programming.

In this chapter we're going to look at a number of patterns to improve the performance and usefulness of JavaScript on the client. I'm not sure that all of these can be thought of as patterns in the strictest sense. They are, however, important and worth mentioning.

The concepts we'll examine in this chapter are as follows:

  • Sending JavaScript
  • Plugins
  • Multithreading
  • Circuit breaker pattern
  • Back-off
  • Promises

Sending JavaScript

Communicating JavaScript to the client seems to be a simple proposition: so long as you can get the code to the client it doesn't matter how that happens, right? Well not exactly. There are actually a number of things that need to be considered when sending JavaScript to the browser.

Combining files

Way back in Chapter 2, Organizing Code, we looked at how to build objects using JavaScript, although opinions on this vary. I consider it to be good form to have a one-class-to-one-file organization of my JavaScript or really any of my object oriented code. By doing this, it makes finding code easy. Nobody needs to hunt through a 9000 line long JavaScript file to locate that one method. It also allows for a hierarchy to be established again allowing for good code organization. However, good organization for a developer is not necessarily good organization for a computer. In our case having a lot of small files is actually highly detrimental. To understand why, you need to know a little bit about how browsers ask for and receive content.

When you type a URL into the address bar of a browser and hit Enter, a cascading series of events happens. The first thing is that the browser will ask the operating system to resolve the website name to an IP address. On both Windows and Linux (and OSX) the standard C library function gethostbyname is used. This function will check the local DNS cache to see if the mapping from name to address is already known. If it is, then that information is returned. If not, then the computer makes a request to the DNS server one step up from it. Typically, this is the DNS server provided by the ISP but on a larger network it could also be a local DNS server. The path of a query between DNS servers can be seen here:

Combining files

If a record doesn't exist on that server then the request is propagated up a chain of DNS servers in an attempt to find one that knows about the domain. Eventually the propagation stops at the root servers. These root servers are the stopping point for queries – if they don't know who is responsible for DNS information for a domain then the lookup is deemed to have failed.

Once the browser has an address for the site it opens up a connection and sends a request for the document. If no document is provided, then a / is sent. Should the connection be a secure one, then negotiation of SSL/TSL is performed at this time. There is some computational expense to setting up an encrypted connection but this is slowly being fixed.

The server will respond with a blob of HTML. As the browser receives this HTML it starts to process it; the browser does not wait for the entire HTML document to be downloaded before it goes to work. If the browser encounters a resource that is external to the HTML it will kick off a new request to open another connection to the web server and download that resource. The maximum number of connections to a single domain is limited so that the web server isn't flooded. It should also be mentioned that setting up a new connection to the web server carries overhead. The flow of data between a web client and server can be seen in this illustration:

Combining files

Connections to the web server should be limited to avoid paying the connection setup costs repeatedly. This brings us to our first concept: combining files.

If you've followed the advice to leverage namespaces and classes in your JavaScript, then putting all of your JavaScript together in a single file is a trivial step. One need only concatenate the files together and everything should continue to work as normal. Some minor care and attention may need to be paid to the order of inclusion, but not typically.

The previous code we've written has been pretty much one file per pattern. If there is a need for multiple patterns to be used, then we could simply concatenate the files together. For instance, the combined builder and factory method patterns might look like the following:

var Westeros;
(function (Westeros) {
  (function (Religion) {
      …
  })(Westeros.Religion || (Westeros.Religion = {}));
  var Religion = Westeros.Religion;
})(Westeros || (Westeros = {}));
(function (Westeros) {
  var Tournament = (function () {
    function Tournament() {
  }
  return Tournament;
})();
Westeros.Tournament = Tournament;
…
})();
Westeros.Attendee = Attendee;
})(Westeros || (Westeros = {}));

The question may arise as to how much of your JavaScript should be combined and loaded at once. It is a surprisingly difficult question to answer. On one hand it is desirable to front load all the JavaScript for the entire site when users first arrive at the site. This means that users will pay a price initially but will not have to download any additional JavaScript as they travel about the site. This is because the browser will cache the script and reuse it instead of downloading it from the server again. However, if users only visit a small subset of the pages on the site then they will have loaded a great deal of JavaScript that was not needed.

On the other hand, splitting up the JavaScript means that additional page visits incur a penalty for retrieving additional JavaScript files. There is a sweet spot somewhere in the middle of these two approaches. Script can be organized into blocks that map to different sections of the website. This can be a place where using proper name spacing will come in handy once again. Each namespace can be combined into a single file and then loaded as users visit that part of the site.

In the end, the only approach that makes sense is to maintain statistics about how users move about the site. Based on this information an optimal strategy for finding the sweet spot can be established.

Minification

Combining JavaScript into a single file solves the problem of limiting the number of requests. However, each request may still be large. Again we come to a schism between what makes code fast and readable by humans and what makes it fast and readable by computers.

We humans like descriptive variable names, bountiful whitespace, and proper indentation. Computers don't care about descriptive names, whitespace, or proper indentation. In fact, these things increase the size of the file and thus decrease the speed at which the code can be read.

Minification is a compile step that transforms the human readable code into smaller, but equivalent, code. External variables' names remain the same, as the minifier has no way to know what other code may be relying on the variable names remaining unchanged.

As an example, if we start with the composite code from Chapter 4, Structural Patterns, the minified code looks like the following:

var Westros;(function(Westros){(function(Food){var SimpleIngredient=(function(){function SimpleIngredient(name,calories,ironContent,vitaminCContent){this.name=name;this.calories=calories;this.ironContent=ironContent;this.vitaminCContent=vitaminCContent}SimpleIngredient.prototype.GetName=function(){return this.name};SimpleIngredient.prototype.GetCalories=function(){return this.calories};SimpleIngredient.prototype.GetIronContent=function(){return this.ironContent};SimpleIngredient.prototype.GetVitaminCContent=function(){return this.vitaminCContent};return SimpleIngredient})();Food.SimpleIngredient=SimpleIngredient;var CompoundIngredient=(function(){function CompoundIngredient(name){this.name=name;this.ingredients=new Array()}CompoundIngredient.prototype.AddIngredient=function(ingredient){this.ingredients.push(ingredient)};CompoundIngredient.prototype.GetName=function(){return this.name};CompoundIngredient.prototype.GetCalories=function(){var total=0;for(var i=0;i<this.ingredients.length;i++){total+=this.ingredients[i].GetCalories()}return total};CompoundIngredient.prototype.GetIronContent=function(){var total=0;for(var i=0;i<this.ingredients.length;i++){total+=this.ingredients[i].GetIronContent()}return total};CompoundIngredient.prototype.GetVitaminCContent=function(){var total=0;for(var i=0;i<this.ingredients.length;i++){total+=this.ingredients[i].GetVitaminCContent()}return total};return CompoundIngredient})();Food.CompoundIngredient=CompoundIngredient})(Westros.Food||(Westros.Food={}));var Food=Westros.Food})(Westros||(Westros={}));

You'll notice that all the spacing has been removed and that any internal variables have been replaced with smaller versions. At the same time, you can spot some well-known variable names have remained unchanged.

Minification saved this particular piece of code 40%. Compressing the content stream from the server using gzip, a popular approach, is lossless compression. That means that there is a perfect bijection between compressed and uncompressed. Minification, on the other hand, is a lossy compression. There is no way to get back to the unminified code from just the minified code once it has been minified.

If there is a need to return to the original code, then source maps can be used. A source map is a file that provides a translation from one format of code to another. It can be loaded by the debugging tools in modern browsers to allow you to debug the original code instead of unintelligible minified code. Multiple source maps can be combine to allow for translation from, say, minified code to unminified JavaScript to TypeScript.

There are numerous tools which can be used to construct minified and combined JavaScript. Gulp and Grunt are JavaScript-based tools for building a pipeline which manages JavaScript assets. Both these tools call out to external tools such as Uglify to do the actual work. Gulp and Grunt are the equivalent to GNU Make or Ant.

Content Delivery Networks

The final delivery trick is to make use of Content Delivery Networks (CDNs). CDNs are distributed networks of hosts whose only purpose is to serve out static content. In much the same way that the browser will cache JavaScript between pages on the site, it will also cache JavaScript that is shared between multiple web servers. Thus, if your site makes use of jQuery, pulling jQuery from a well-known CDN such as https://code.jquery.com/ or Microsoft's ASP.net CDN may be faster as it is already cached. Pulling from a CDN also means that the content is coming from a different domain and doesn't count against the limited connections to your server. Referencing a CDN is as simple as setting the source of the script tag to point at the CDN.

Once again, some metrics will need to be gathered to see whether it is better to use a CDN or simply roll libraries into the JavaScript bundle. Examples of such metrics may include the added time to perform additional DNS lookup and the difference in the download sizes. The best approach is to use the timing APIs in the browser.

The long and short of distributing JavaScript to the browser is that experimentation is required. Testing a number of approaches and measuring the results will give the best result for end users.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset