Skip to content

Code trimming using has.js feature detection

chuckdumont edited this page Oct 19, 2012 · 14 revisions

The Aggregator will remove code that is not needed for a particular request from JavaScript modules in order to minimize their size. How does the Aggregator decide if code is not needed? By scanning the code for has.js feature tests and removing the unneeded code based on the feature set specified in the request. The HTTP transport plug-in extension is responsible for passing along the feature set specified on the browser in the request. The default HTTP transport does this by encoding the feature set in the URL as a query arg, or optionally specifying it in a cookie in order to conserve URL space for specifying modules.

Has.js Feature Detection

Feature detection using has.js is performed by calling the function "has" and specifying the name of the feature you want to test for. If the function returns true, then the feature is supported. If it returns false, then the feature is not supported. If the function returns undefined, then no test for the specified feature has been registered. For example, the following code tests if the JavaScript String object supports the trim function and either uses the built in trim function or else uses an alternate implementation based on regular expressions.

mylibrary.trim = has("string-trim") ? function(str){
    return (str || "").trim();
} : function(str){
    /* do the regexp based string trimming */
}

Code trimming based on feature detection

During the JavaScript minification process, after the JavaScript has been parsed into an AST (Abstract Syntax Tree), but before optimizations are performed, the compiler calls a the Aggregator provided custom compiler pass module which performs code trimming by scanning the parsed AST generated by the compiler and searches for "has" function calls that specify a single string literal function parameter. For each node found, it looks to see if the string is specified in the feature set provided in the request. If it is, then the function call node is replaced with a node representing the JavaScript literal true or false depending on the the value of the feature, and the compiler then removes any resulting dead code during its optimization pass.

For the above example, and assuming that the "sting-trim" feature is true, the resulting code after trimming and optimization would look like the following:

mylibrary.trim=function(a){return(a||"").trim()}

Caveats

Feature trimming is done only for features that have been defined on the client with a call to has.add(). If there is no test for a feature at the time that a module is requested, then code trimming is not performed for the feature. The reason is that the absence of a feature test could easily be due to the fact that the code that adds the feature test hasn't yet been loaded or run at the time that the request for the module is made. Removing code based on undefined features could cause javascript errors if code is removed for a feature that is later needed. This means that in order to make the most of the feature trimming capabilities, it is necessary to define negative feature tests as well as positive feature tests. For example, the following javascript code will work perfectly well on the client:

if (typeof "".trim === 'function') {
    has.add("string-trim", true);
}

but it means that the test for the string-trim feature will not be added if the feature is not available, and that code trimming for the feature will not be performed for clients where it is not available. It is better to make sure that a test is added in all cases

has.add("string-trim", typeof "".trim === 'function');

so that the server can do code trimming for either the true or false cases.

It would be great if that were all there were to it, but unfortunately, it gets messier. Although the has.js spec (such as it is) specifies that the has function return a "truthy" value if the feature is supported, in practice, implementations, including the one provided with Dojo, fail to coerce the value returned by the test for the feature to a boolean type. This enables you to write code such as the following:

if (has("IE") <= 6) {
   // Handle IE 6 and below
} else {
   // IE7 and above
}

Obviously, using the result of the has function call coerced to a boolean value will not yield the desired results, and passing the non-boolean result of the function call to the server is not a viable option since there is nothing to prevent a has function from returning the entire text of War and Peace. Consequently, the Aggregator will only perform code trimming for feature tests that unambiguously treat the value returned by the has call as a boolean type. This, unfortunately, does not include the following:

if (has("myFeature") == true)

because (5 == true) does not yield the same result as (Boolean(5) == true). So, in order to take full advantage of code trimming by the Aggregator, avoid explicit comparisons of feature tests with true or false. The following are all examples of tests that will be trimmed by the Aggregator if a value for the feature(s) is specified in the request.

if (has("foo") say(foo); else say("!foo");

if (!has("foo") say("!foo");

if (has("foo") && has("bar")) { say(foo + bar) }

if (has("foo") || has("bar")) { say(foo||bar);}

say(has("foo") ? foo : "!foo");

URL length considerations

Depending on the size and complexity of the application, and what frameworks are employed, the size of the feature list on the client may become significant. This can present challenges when trying to squeeze the feature list, together with the requested module names, into the URL without exceeding the maximum URL length. The Aggregator provides a couple of options for dealing with this constraint.

Sending the feature list in a cookie

The Aggregator provides the ability to utilize a browser cookie for sending the feature list to the server when requesting modules, freeing up space in the URL to specify modules. To employ this capability, simply require the module combo/featureCookie in your initial require call or in the deps property of your client-side AMD loader config. When this module is loaded, the feature list will be sent to the server in a cookie named has for all Aggregator requests. In order to ensure cache coherency, an MD5 hash of the cookie is included in the URL using the hashash query arg. The "combo/featureCookie" module depends on the "dojo/cookie" and "dojox/md5" modules.

Filtering the feature list

You can also limit the size of the feature list that is sent to the server by providing a filter function using the featureFilter property of the loader extension config