Browser Detection: Necessary or Negligent?

It is still a controversial issue today despite the recent evolutions in the industry including new modern browsers and new resources such as jQuery (as of version 1.3) that now entirely base their implementations on feature testing. However, somehow the practice of browser detection seems to still be alive and well, whether it be new browsers bringing about new bugs, features that are difficult to detect, or simply shortcuts spawned out of pure laziness, the practice is not only being used but defended. Browser sniffing can be found around just about every corner you look – from the smallest of websites, to your favourite JavaScript library (Mootools, Prototype, YUI, etc.) utilized in corporate giants such as Chrysler, Gamespot, and Yahoo!

What is Browser Detection?

Browser detection or sniffing is the practice of detecting the browser runtime environment in which JavaScript is being executed. This helps to restrict execution of certain fragments of code to that browser based on features or bugs that are believed to be associated with that specific browser. This exercise is not only restricted to the type of browser (IE, Firefox, Opera, Safari, etc.) but also the browser version and the operating system. The most common method of detecting the browser is via the userAgent property of the navigator object, for example, the following is the output of the user agent string of your browser:

As you can tell, you will be required to parse that string to get a definitive browser and version out of it.  Some developers will choose to ignore the user agent electing instead to facilitate unrelated object inference to detect the browser, such as ActiveXObject for Internet Explorer, despite common belief this should not be considered object detection.

The goal of browser detection is too provide a simple method in which to detect a browser’s native feature or method support (XMLHttpRequest, ActiveXObject, etc.), object support (event.clientX, event.pageX, etc.), and most importantly bugs (IE can confuse id and name attributes when using document.getElementById()).

The Dangers

The practice of browser detection is nothing new and from the perspective of a relatively new developer to the language, it is a justified solution to obtain cross-browser compatibility which can be so difficult to achieve. However at some point along the way, the exercise has been abused, quickly becoming common routine rather than a last resort, finding its way into scripts where simple object detection would suffice.

The fundamental problem with browser detection in any capacity is the inherent dependency on what is widely believed to be true associations between a browser and a bug or feature. In other words, you are making an assumption about every browser of every user that visits the page, and assuming every feature and bug that is being tested for is a correct affiliation with that browser. These assumptions as always can be very dangerous, your application as a whole may just depend on it.

The flaw in browser detects themselves are their static nature, whether your testing the user agent or relying on a specific object (ActiveXObject) to identify the browser, a static browser detect is only as reliable as your knowledge of the browser environments you currently support at the time of implementation. As browsers continue to evolve, new browser and browser versions released, and API’s updated, your application will become a fragile implementation at the mercy of the evolution. What were previously thought to be true associations between a browser and a feature or bug are now false and inconsistent.

Browser spoofing is an issue developers have been tackling ever since the days of Internet Explorer and Netscape Navigator. What is browser spoofing? Well, quite simply it is the ability for one browser to attempt to imitate another via the user agent string. If you re-examine the example I provided above of what your user agent is, you may notice (depending on the browser your using) hints of other browsers. A quick test in the various browsers for me reveals words such as “Mozilla” in IE7 and  “Gecko” and “Safari” in Chrome. This makes it very difficult to create a reliable and consistent browser detect, requiring a developer to constantly change their methods of browser detection. To view the history of the user agent string and browser spoofing, see http://webaim.org/blog/user-agent-string-history/

The ultimate goal of any application is to increase and maintain traffic, this starts with attempting to support as many browsers as possible. Typically, browser detection infers a specific list of supported browsers as is the case with most JavaScript libraries. However, this not only isolates your browser support but also alienates alternative browsers and browser versions that may otherwise be fully capable of supporting your application. Upgrading your browser version may be all that is required to break the browser sniff and all subsequent dependencies. Ideally, you should aim to support any browser that supports only the features your application utilizes, and offers a graceful degradation for those that do not.

It should also be noted that the greatest discrepancies between browsers is in rendering of the web page, which is not so much a JavaScript issue but a CSS and markup issue, so lets keep it that way. Making adjustments to various measured CSS properties such as width and height in JavaScript based on the type of browser should not be necessary. Of course there are the well known asterisk and underscore hacks, as well as many other browser specific workarounds in CSS, but that is another topic for another day.

Feature Testing

The ultimate solution and alternative to browser detection is feature testing, the simple means of testing for a “feature” before utilizing it in some capacity. The technique is often applied as direct object, method, or typeof inference as it relates to the feature within the browser environment during runtime. Ultimately, the reliability of such a test case lies solely in it’s ability to execute successfully and provide a definitive boolean value in which to compare against. However, there is a right way and wrong way in which to approach this technqiue, for instance, evaluting support for event handlers typically will involve a test case for standards compliant browsers such as Firefox and falls back to IE’s implementation if addEventListener is not supported:

function addEvent(el, type, fn)
    if(el.addEventListener){
        el.addEventListener(type, fn, false);
    }else{
        el.attachEvent('on' + type, fn);
    }
}

However, even a perfectly suited feature test as common as this is making an assumption about the user’s browser; if it doesn’t support addEventListener, it must be IE, so lets use the attachEvent implementation. Ideally, a test case should be used for each browser specific implementation, and provide a final fallback on the off chance of a failed test case in an alien browser:

function addEvent(el, type, fn)
    if(el.addEventListener){
        el.addEventListener(type, fn, false);
    }else if(el.attachEvent){
        el.attachEvent('on' + type, fn);
    }else{
        el['on' + type] = fn;
    }
}

Ultimately, this method combined with an unobtrusive implementation will almost certainly provide a reliable application suited for both browsers that support the method and those that don’t via graceful degradation, including the crowd that has disabled JavaScript.

The wild card are the various bugs or quirks; supported implementations that either do not perform or behave as expected. More often than not, a bug exists within a host method, meaning the only way it can possibly be tested is to actually execute the method in a meaningful test case, for example:

function(){
    try{
        var children = document.documentElement.childNodes;
        return Array.prototype.slice.call(children) instanceof Array;
    }catch(e){}
    return false;
}

This particular test determines support for a native nodelist to array conversion. The method utilizes a try-catch block which typically should be avoided, primarily because in a context such as this, an assumption is being made about what it means when a test fails and the exception is caught. We assume that the exception was thrown and the method returned false because the browser doesn’t support native array conversions. However the method would return the same if an error occurred due to a bug else where in the try-catch block, this makes for fragile code in future browser environments. Although the solution is not ideal, it is the best JavaScript will allow us do at this point in time so it will have to do.

Despite difficult and often cumbersome, there is almost always a test case available in some capacity capable of determining browser support. To view more common bugs as well as their respective test cases, have a look at Juriy Zaytsev’s fantastic resource.

Feature testing isn’t the perfect solution – there is no perfect solution, however from a practical standpoint, it is your best bet to ensure a reliable code base. As it relates to testing bugs, the dependency in the test will lie solely in the method in which you test the feature. The bottom line is if there is one thing that a developer should know; it’s the environment that he or she is dealing with. It is of far greater importance that a developer know the bugs and how to individually avoid them than to lump them all together based on assumptions in an eventual failure. To read more about feature testing, see Peter Michaux‘s excellent and all inclusive article on feature detection.

Conclusion

Feature testing may not be simple and in some cases may require an elaborate and complicated test case, however it is the obvious answer. As oppossed to browser detection, it doesn’t draw on associations, assumptions, or false positives that are believed to be true affilations with a specific browser to create fragile test cases. Instead, feature testing relies on what it knows about the browser environment during runtime, not the precieved browser support at the time of implementation. The philosophy here is simple; if your unsure that a specific object/method is supported in the native environment or performs as expected, try it and let the test case tell you – don’t tell it. Resorting to browser detection should no longer be considered a last resort, but bad practice.

This entry was posted on Sunday, March 29th, 2009 at 5:27 PM and is filed under Articles, JavaScript. You can follow any responses to this entry through the RSS 2.0 feed. You can leave a response, or trackback from your own site.

Comments (1)

  • jQuery 1.3 is not based on feature testing (not even close.) They’ve just moved on to weak object inferences. As such, they are almost a decade behind the times.

Leave a Reply