Performance Innumeracy & False Positives

tl;dr version: the web is waaaay too slow, and every time you write something off as “just taking a couple of milliseconds”, you’re part of the problem. Good engineering is about tradeoffs, and all engineering requires environmental assumptions — even feature testing. In any case, there are good, reliable ways to use UA detection to speed up feature tests in the common case, which I’ll show, and to which the generic arguments about UA vs. feature testing simply don’t apply. We can and should go faster. Update: Nicholas Zackas explains it all, clearly, in measured form. Huzzah!

Performance Innumeracy

I want to dive into concrete strategies for low-to-no false positive UA matching for use in caching feature detection results, but first I feel I need to walk back to some basics since I’ve clearly lost some people along the way. Here are some numbers every developer (of any type) should know, borrowed from Peter Norvig’s indispensable “Teach Yourself To Program In Ten Years”:

Approximate timing for various operations on a typical PC:

execute typical instruction 1/1,000,000,000 sec = 1 nanosec
fetch from L1 cache memory 0.5 nanosec
branch misprediction 5 nanosec
fetch from L2 cache memory 7 nanosec
Mutex lock/unlock 25 nanosec
fetch from main memory 100 nanosec
send 2K bytes over 1Gbps network 20,000 nanosec
read 1MB sequentially from memory 250,000 nanosec
fetch from new disk location (seek) 8,000,000 nanosec
read 1MB sequentially from disk 20,000,000 nanosec
send packet US to Europe and back 150 milliseconds = 150,000,000 nanosec

That data’s a bit old — 8ms is optimistic for a HD seek these days, and SSD changes things — but the orders of magnitude are relevant. For mobile, we also need to know:

fetch from flash storage 1,300,000 nanosec
60hz time slice 16,000,000 nanosec
send packet outside of a (US) mobile carrier network and back 80-800 milliseconds = 80,000,000 – 800,000,000 nanosec

The 60hz number is particularly important. To build UI that feels not just fast, but instantly responsive, we need to be yielding control back to our primary event loop in less than 16ms, all the time, every time. Otherwise the UI will drop frames and the act of clicking, tapping, and otherwise interacting with the app will seem “laggy” or “janky”. Framing this another way, anything your webapp blocks on for more than 16ms is the enemy of solid, responsive UI.

Why am I blithering on and on about this? Because some folks continue to mis-prioritize the impact of latency and performance on user satisfaction. Google (my employer, who does not endorse this blog or my private statements in any way) has shown that seemingly minor increases in latency directly impact user engagement and that major increases in latency (> 500ms) can reduce traffic and revenue significantly. Latency then, along with responsiveness (do you drop below 60hz?), is a key metric for measuring the quality of an web experience. It’s no accident that Google employs Steve Souders to help evangelize the cause of improving performance on the web, and has gone so far as to build products like Chrome & V8 who have as a core goal to the web faster. A faster web is a better web. Full stop.

That’s why I get so deeply frustrated when we get straw-man based, data-challenged advocacy from the maintainers of important bits of infrastructure:

This stuff is far from easy to understand; even just the basics of feature detection versus browser detection are quite confusing to some people. That’s why we make libraries for this stuff (and, use browser inference instead of UA sniffing). These are the kind of efforts that we need, to help move the web forward as a platform; what we don’t need is more encouragement for UA sniffing as a general technique, only to save a couple of milliseconds. Because I can assure you that the Web never quite suffered, technologically, from taking a fraction of a second longer to load.

What bollocks. Not only did I not encourage UA sniffing “as a general technique”, latency does in fact hurt sites and users — all the time, every day. And we’re potentially not talking about “a couple of milliseconds” here. Remember, in the context of mobile devices, the CPUs we’re on are single-core and clocked in the 500mhz-1ghz range, which directly impacts the performance of single-threaded tasks like layout and JavaScript execution — which by the way happen in the same thread. In my last post I said:

…if you’re a library author or maintainer, please please please consider the costs of feature tests, particularly the sort that mangle DOM and or read-back computed layout values

Why? Because many of these tests inadvertently force layout and style re-calculation. See for instance this snippet from has.js:

if(has.isHostType(input, "click")){
  input.type = "checkbox";
  input.style.display = "none";
  input.onclick = function(e){
    // ...
  };
  try{
    de.insertBefore(input, de.firstChild);
    input.click();
    de.removeChild(input);
  }catch(e){}
  // ...
}

Everything looks good. The element is display: none; so it shouldn’t be generating render boxes when inserted into the DOM. Should be cheap, right? Well, lets see what happens in WebKit. Debugging into a simple test page with equivalent code shows that part of the call stack looks like:

#0	0x0266267f in WebCore::Document::recalcStyle at Document.cpp:1575
#1	0x02662643 in WebCore::Document::updateStyleIfNeeded at Document.cpp:1652
#2	0x026a89fd in WebCore::MouseRelatedEvent::receivedTarget at MouseRelatedEvent.cpp:152
#3	0x0269df03 in WebCore::Event::setTarget at Event.cpp:282
#4	0x026af889 in WebCore::Node::dispatchEvent at Node.cpp:2604
#5	0x026adbcb in WebCore::Node::dispatchMouseEvent at Node.cpp:2885
#6	0x026ae231 in WebCore::Node::dispatchSimulatedMouseEvent at Node.cpp:2816
#7	0x026ae3f1 in WebCore::Node::dispatchSimulatedClick at Node.cpp:2837
#8	0x02055bb5 in WebCore::HTMLElement::click at HTMLElement.cpp:767
#9	0x022587e6 in WebCore::HTMLInputElementInternal::clickCallback at V8HTMLInputElement.cpp:707
...

Document::recalcStyle() can be very expensive, and unlike painting, it blocks input and other execution. And the cost is at page loading is likely to be much higher than other times as there will be significantly more new styles streamed in from the network to satisfy for each element in the document when this is called. This isn’t a full layout, but it’s most of the price of one. Now, you can argue that this is a WebKit bug and I’ll agree — synthetic clicks should probably skip this — but I’m just using this as an illustration to show that what browsers are doing on your behalf isn’t always obvious. Once this bug is fixed, this test may indeed be nearly free, but it’s not today. Not by a long shot.

Many layouts in very deep and “dirty” DOMs can take ten milliseconds or more, and if you’re doing it from script, you’re causing the system to do lots of work which it’s probably going to need to throw away later when the rest of your markup and styles show up. Your average, dinky test harness page likely under-counts the cost of these tests, so when someone tells me “oh, it’s only 30ms”, not only do my eyes bug out at the double-your-execution-budget-for-anything number, but also the knowledge that in the real world, it’s probably a LOT worse. Just imagine this happening in a deep DOM on a low-end ARM-powered device where memory pressure and a single core are conspiring against you.

False Positives

My last post concerned how you can build a cache to eliminate many of these problems if and only if you build UA tests that don’t have false positives. Some commenters can’t seem to grasp the subtlety that I’m not advocating for the same sort of lazy substring matching that has deservedly gotten such a bad rap.

So how would we build less naive UA tests that can have feature tests behind them as fallbacks? Lets look at some representative UA strings and see if we can’t construct some tests for them that give us sub-version flexibility but won’t pass on things that aren’t actually the browsers in question:

IE 6.0, Windows:

Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)

FF 3.6, Windows:

Mozilla/5.0 (Windows; U; Windows NT 5.1; de; rv:1.9.2.13) Firefox/3.6.13

Chrome 8.0, Linux:

Mozilla/5.0 (X11; U; Linux x86_64; en-US) AppleWebKit/534.10 (KHTML, like Gecko) Ubuntu/10.10 Chromium/8.0.552.237 Chrome/8.0.552.237 Safari/534.10

Safari 5.0, Windows:

Mozilla/5.0 (Windows; U; Windows NT 6.1; sv-SE) AppleWebKit/533.19.4 (KHTML, like Gecko) Version/5.0.3 Safari/533.19.4

Some features start to jump out at us. The “platform” clauses — that bit in the parens after the first chunk — contains a lot of important data and a lot of junk. But the important stuff always comes first. We’ll need to allow but ignore the junk. Next, stuff after platform clauses is good, has defined order, and can be used to tightly form a match for browsers like Safari and Chrome. With this in mind, we can create some regexes that don’t allow much in the way of variance but do allow sub-minor version to match so we don’t have to update these every month or two:

IE60 = /^Mozilla\/4\.0 \(compatible; MSIE 6\.0; Windows NT \d\.\d(.*)\)$/;
FF36 = /^Mozilla\/5\.0 \(Windows; U;(.*)rv\:1\.9\.2.(\d{1,2})\)( Gecko\/(\d{8}))? Firefox\/3\.6(\.\d{1,2})?( \(.+\))?$/;
CR80 = /^Mozilla\/5\.0 \((Windows|Macintosh|X11); U;.+\) AppleWebKit\/534\.10 \(KHTML\, like Gecko\) (.+)Chrome\/8\.0\.(\d{3})\.(\d{1,3}) Safari\/534\.10$/;

These look pretty wordy, and they are, because they’re designed NOT to let through things that we don’t really understand. This isn’t just substring matching on the word “WebKit” or “Chrome”, this is a tight fit against the structure of the entire string. If it doesn’t fit, we don’t match, and our cache doesn’t get pre-populated. Instead, we do feature detection. Remember, false positives here are the enemy, so we’re using “^” and “$” matches to ensure that the string has the right structure all the way through, not just at some random point in the middle, which UA’s that parade around as other browsers tend to do.

Here’s some sample code that incorporates the approach:

(function(global){
 
    // The map of available tests
    var featureTests = {
        "audio": function() {
            var audio = document.createElement("audio");
            return audio && audio.canPlayType;
        },
        "audio-ogg": function() { /*...*/ }
        // ...
    };
 
    // A read-through cache for test results.
    var testCache = {};
 
    // An (exported) function to run/cache tests
    global.ft = function(name) {
        return testCache[name] = (typeof testCache[name] == "undefined") ?
                                    featureTests[name]() :
                                    testCache[name];
    };
 
    // Tests for 90+% of current browser usage
 
    var ua = (global.navigator) ? global.navigator.userAgent : "";
 
    // IE 6.0/WinXP:
    var IE60 = /^Mozilla\/4\.0 \(compatible; MSIE 6\.0; Windows NT \d\.\d(.*)\)$/;
    if (ua.search(IE60) == 0) {
        testCache = { "audio": 1, "audio-ogg": 0 /* ... */ };
    }
 
    // IE 7.0
    // ...
    // IE 8.0
    // ...
 
    // IE 9.0 (updated with fix from John-David Dalton)
    var IE90 = /^Mozilla\/5\.0 \(compatible; MSIE 9\.0; Windows NT \d\.\d(.*)\)$/;
    if (ua.search(IE90) == 0) {
        testCache = { "audio": 1, "audio-ogg": 0 /* ... */ };
    }
 
    // Firefox 3.6/Windows
    var FF36 = /^Mozilla\/5\.0 \(Windows; U;(.*)rv\:1\.9\.2.(\d{1,2})\)( Gecko\/(\d{8}))? Firefox\/3\.6(\.\d{1,2})?( \(.+\))?$/;
    if (ua.search(FF36) == 0) {
        testCache = { "audio": 1, "audio-ogg": 1 /* ... */ };
    }
 
    // Chrome 8.0
    var CR80 = /^Mozilla\/5\.0 \((Windows|Macintosh|X11); U;.+\) AppleWebKit\/534\.10 \(KHTML\, like Gecko\) (.+)Chrome\/8\.0\.(\d{3})\.(\d{1,3}) Safari\/534\.10$/;
    if (ua.search(FF36) == 0) {
        testCache = { "audio": 1, "audio-ogg": 1 /* ... */ };
    }
 
    // Safari 5.0 (mobile)
    var S5MO = /^Mozilla\/5\.0 \(iPhone; U; CPU iPhone OS \w+ like Mac OS X; .+\) AppleWebKit\/(\d{3,})\.(\d+)\.(\d+) \(KHTML\, like Gecko\) Version\/5\.0(\.\d{1,})? Mobile\/(\w+) Safari\/(\d{3,})\.(\d+)\.(\d+)$/;
    if (ua.search(FF36) == 0) {
        testCache = { "audio": 1, "audio-ogg": 0 /* ... */ };
    }
 
    // ...
 
})(this);

New versions of browsers won’t match these tests, so we won’t break libraries in the face of new UAs — assuming the feature tests also don’t break, which is a big if in many cases — and we can go faster for the majority of users. Win.

33 Comments

  1. Posted February 3, 2011 at 12:00 pm | Permalink

    Nice.

    One question. Where did the 16ms number come from? I don’t see a reference.

  2. Posted February 3, 2011 at 12:07 pm | Permalink

    It’s 1000ms/60frames.

  3. Posted February 3, 2011 at 12:15 pm | Permalink

    You’re absolutely right, performance is the only metric that really matters. Let’s all go back to writing pure HTML sites and drop this lunacy of time-consuming resources like “CSS” and “JavaScript.”

  4. Posted February 3, 2011 at 12:22 pm | Permalink

    Sorry, what I meant was – have there been studies on this to back up the 16ms number?

    Didn’t that number used to be 100ms? Maybe that was eons ago. Have humans learned to expect smaller latency since then? I could believe it – after all, 16ms is an eternity when it comes to audio latency, our devices keep getting faster, we’re consuming more and more caffeine every year, etc.

  5. Posted February 3, 2011 at 12:37 pm | Permalink

    Thanks for the numbers breakdown and pseudo-code, I think it very much accurately displays what I think you’ve been trying to say since your first article.

    My only apprehension to this method is that the cost may just be being shifted to a new spot. (albeit, then cached)

    When you build your table of pre-cached browser UA strings, you’ll likely not want to spend too much by way of raw bytes to do so (since these have to be downloaded quickly, so the correct polyfill or interface can be loaded based on the feature support early in the site load).

    So the obvious solution (and one you eluded to) is to only put the most popular browsers in your cache. IE6-9, a few FFs, a few Safaris, a couple of the latest iphone and androids UAs. That way the most people get the shortcut.

    The only problem I can see with that, is that the slowest browsers are _not_ the most popular ones. So we may be taking a shortcut the majority of the time, in a place where it didn’t actually matter to begin with. Then we end up running the slow feature tests on the old blackberry device where the shortcut really would have come in handy.

    Which is ok. Because maybe we could just switch our shortcuts around to ignore chrome and new IEs and Safaris, and really target old browsers more, for the shortcuts. (since they’re the browsers that are probably going to end up needing extra treatment anyways). I think this list changes though, depending on your use-case. I just wanted to point out that the 90% coverage of browsers might not do you the most good.

    Your code (_just_ the part that matches UA strings at the end, which admittedly leaves out IE7 and 8), wrapped in a immediately invoked function expression closure compiles to: 293 bytes gzipped.

    That’s a latency free 293 bytes since it’s part of something you already downloaded, but it’s still not free. I’d think you’d have to weight the cost of 293 bytes (likely more since there should probably be quite a few more browsers in the pre-cache) and how long that would take to download into your equation first.

    TL;DR ———

    I think some combination of both techniques is ideal, much like you are saying, but I think that depending on your use-case, it changes each time and may be entirely unpredictable. Hooray.

  6. evan
    Posted February 3, 2011 at 12:38 pm | Permalink

    I assumed he used 60 Hz because that’s what most monitors refresh at.

  7. Posted February 3, 2011 at 12:42 pm | Permalink

    hey Alex,

    Performance work is always a tradeoff. That you have to make hard choices and give something up is no shock = )

  8. Posted February 3, 2011 at 12:42 pm | Permalink

    Facebook’s HTML5 Tech Talk last week talked about using 30fps as a baseline for interactions to feel natural – in a gaming environment – which requires 33ms interactions although faster certainly is better. You can see the whole presentation here http://www.livestream.com/facebookeducation/video?clipId=pla_98824c1a-a2e1-4831-9b9e-c5cd23f5d8eb

  9. Posted February 3, 2011 at 12:44 pm | Permalink

    Great writeup and explanations.

    I don’t really understand Faruk’s comments at all. This article is showing techniques and reasons to care about specific performance issues that combine to either produce the best or worst user experiences.

    The general point about tailoring your application to the environment is valuable and valid. I think in a lot of cases we just are happy to see that something works, and as developers focus purely on the “Aha” moment and rarely use our own products as a fresh user would (meaning different browsers, not a quad core mac pro, etc).

  10. Posted February 3, 2011 at 12:45 pm | Permalink

    hey Patrick,

    Sorry, yeah, I implicitly meant visual latency. For other uses cases, 16ms is faaar too long, and even visually it’s a far upper bound. Lower is always better, but if your screen only blits at 60hz, you can get near 16ms of execution time to play with.

    Sorry I wasn’t clearer.

  11. Posted February 3, 2011 at 1:28 pm | Permalink

    If you need the Opera User Agent Generic strings, there are available at http://my.opera.com/community/openweb/idopera/

    DESKTOP
    Opera/9.80 ($OS; U; $LANGUAGE) Presto/$PRESTO_VERSION Version/$VERSION

    MOBILE
    Opera/9.80 ($OS; Opera Mobi/$BUILD_NUMBER; U; $LANGUAGE) Presto/$PRESTO_VERSION Version/$VERSION

    MINI
    Opera/9.80 (J2ME/MIDP; Opera Mini/$CLIENT_VERSION/$SERVER_VERSION; U; $LANGUAGE) Presto/$PRESTO_VERSION

  12. Posted February 3, 2011 at 1:59 pm | Permalink

    I agree there are some cases you can say *with certainty* that for a given UA string, its expected result for a given feature/bug test.

    But I’d agree with Alex here that the combo of regular expressions and support lookup hashes introduce enough extra code that it’s cost on the wire would probably outweigh the runtime performance benefit.

    However.. if this happens on the serverside, then we’re mostly in the clear from that problem.

    Worth noting: All this stuff is very closely aligned to the JSKB idea: http://google-caja.googlecode.com/svn/trunk/doc/html/jskb.html

    But thus far the problem with that is everyone has their own UA parsing logic. As long as people parse UA differently, it’s better for them to be using reliable feature detection code. But I think we can solve the UA parsing inaccuracies as well.

    Since this conversation began, I’ve talked to Lindsey Simon about this.. He wrote the UA Parser that’s in use on Browserscope and some other properties: http://code.google.com/p/ua-parser/
    The end goal is basically a port of the regex’s and parse code to all primary web languages, plus a PubSubHubbub-style subscription service (free).. kinda like virus signatures, that keep you up to date with any emerging browsers. I think having a strong set of regex’s that have community approval.. that’s the only way to execute on this plan.

    In general, it seems like taking the good work WURFL has done and expanding it to be much better at UA detection, and then expanding the capabilities to capture the interesting client-side stuff we’re curious about.

    Anyway, Lindsey and I are quite enthused about this idea, and think it can combo well with clientside feature detection. If anyone else is game, let me know.

  13. Posted February 3, 2011 at 2:10 pm | Permalink

    Hey Paul,

    Hmm…the amount of code here for the caches vs. the amount you need for the tests themselves is relatively small. You can also only provide tests for the most popular browsers and cache results only for the most frequently hit tests (or the ones that are most likely to do expensive operations). As long as the cache is read-through, you have complete flexibility.

    In any case, if you’re doing this server side, you already have better options as you can afford hash-based UA lookup and a much more complete UA dictionary, allowing you to skip sending the feature test code in nearly every case anyway.

    Regards

  14. Posted February 3, 2011 at 4:14 pm | Permalink

    Hi Alex,

    I think there are already projects out there that do something close to what you are suggesting. (Caja Web Tools, embed.js, even MooTools)

    I noticed several of your UA sniffs (IE6, IE9, FF3.6, Chrome 8, …) failed UA strings I’ve tested against. UA strings are tricky and getting a correct result is a pain.

    On a side note, has.js tests aren’t necessarily meant to be executed all in one shot. They are designed to allow for lazy testing. This can reduce the initial perf hit by allowing devs to check them when needed and not all up front.

    I dig profiling environments for features, and conditional builds, but I don’t think using the UA alone is the best approach.
    With all the talk of milliseconds and nanoseconds I am reminded of something you wrote in 09, “Fast enough is fast enough, and the bottlenecks are elsewhere in toolkits today.”

  15. Posted February 3, 2011 at 4:26 pm | Permalink

    Hi John-David:

    I’m a big fan of what embed.js is doing.

    I think you’re defining “failure” wrong, or at least differently. The difference between my regexes and what folks normally do is that not matching a particular UA is OK. It’s only failure in this world if you’re missing the majority of your traffic (too many false negatives) or if you falsely match a UA that you shouldn’t. Remember, the inversion I’m pulling here is moving the emphasis away from UA testing that needs to fingerprint every UA to testing that needs to get the right answer most of the time with zero false positives.

    If you’ve got a case where I’m generating a false-positive, would love to know, though.

    Regards

  16. Posted February 3, 2011 at 5:15 pm | Permalink

    > It’s only failure in this world if you’re missing the majority of your traffic (too many false negatives) or if you falsely match a UA that you shouldn’t.

    That’s what I meant. For example your UA sniff for IE9 won’t match IE9 because its UA contains “Mozilla/5″ not “Mozilla/4″.

    > If you’ve got a case where I’m generating a false-positive, would love to know, though.

    I linked to a general example of a false positive in my previous comment. For a false positive with one of your UA sniffs you can look at this. Other browsers like that can be troublesome because their UA strings are so similar.

  17. Posted February 3, 2011 at 6:34 pm | Permalink

    Hey John,

    Thanks for the IE 9 tip. Fixed in the article body. Somewhat humorously, the fact that my test was busted sort of proves the point that strictly-written tests fail closed and therefore fall back to feature testing, which means that things aren’t actually broken, just slower.

    As for SkyFire, Sleipnir, and CometBird, AFAICT, their rendering and JS execution environment *are* the stated versions of FireFox or IE, respectively. Also, the CometBird UA doesn’t pass the regex I posted.

    Looks to me like we’re in good shape: low-to-no false positives (vs. the deployed bulk of browsers) and fast paths most places.

    Thanks again for the fix on the IE9 UA.

    Regards

  18. Posted February 3, 2011 at 7:29 pm | Permalink

    Hi Alex,

    Thanks for this blog post, it really clears up what you meant by “I say you shouldn’t be running tests in UA’s where you can dependably know the answer a-priori.” I think the method of doing the kind of strict UA matching is a lot saner than 99.999% of what’s on the web right now.

    Initially, I suspected your suggestion was more along the lines of: https://gist.github.com/810674 (not a strawman, actual production code from http://slides.html5rocks.com). It makes some assumptions about what browsers can do, caches the “test” result…and locks out IE9, for example. A fine example of short-sighted code.

    I can see that’s not what you’re advocating. That’s a good thing.

    My immediate concern is how this will affect the browser I use on a daily basis, Opera (disclosure, I also am employed by them). Opera is in an interesting position in that it’s got say 2% global market share on Desktop, yet locally and regionally much, much higher (ignoring Mobile for the moment, where some countries are as high as 95% Opera) e.g. Russia and the rest of the former Soviet Bloc at about 30% (http://gs.statcounter.com/#browser-RU-monthly-201001-201101).

    Since it’s got such a small market share here in the US many large sites won’t (and can’t) justify QA costs and outright block the site or serve a crippled version (based on UA sniffing, of course). For example, take Netflix. Despite serving their streaming video via Silverlight (which works with Opera), they outright block the UA. Lucky for me, I can easily tell Opera to “Mask as Firefox” and suddenly my UA is “Mozilla/5.0 (Windows NT 6.1; U; en; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6″. Yay, I get to stream Dirty Jobs now.

    In these situations, I’m now Firefox and a UA matcher like the one described here is going to tell me that I have access to the File API (or whatever…), except I don’t. Not very awesome.

  19. Posted February 3, 2011 at 8:09 pm | Permalink

    Hey Mike,

    So lets consider the things you’re scared about: web developers are doing the wrong thing, and you’re employing a hack to get around it. Fair enough. But the technique I’m describing, and the location in the ecosystem I’m advocating it’s use, is way upstream from the problem you’re hitting. Hopefully, by doing things the way I’m describing, we can keep apps from turning browsers away in the first place since the libraries and tools they depend on “Just Work (TM)” in browsers they don’t understand. The question of what should a bit of code do in the spoof-to-get-around-bad-UA-detection case isn’t something that’s even up for consideration here. Nobody’s advocating that sites should block unknown UAs, and for that matter, nobody’s seriously advocating UA spoofing. What libraries should do, then, is pretty straightforward, and our advice to web developers doesn’t change: just Do The Right Thing and rely on feature tests. The only addendum here is that, when you can, also fast-path the common cases. If we advocate for *that*, then you never hit the problems you’re describing in the first place.

    Regards

  20. Posted February 3, 2011 at 11:31 pm | Permalink

    > As for SkyFire, Sleipnir, and CometBird, AFAICT, their rendering and JS execution environment *are* the stated versions of FireFox or IE, respectively. Also, the CometBird UA doesn’t pass the regex I posted.

    I think the fact that you can get false positives shows how fragile it is. I am sure there are more examples of false positives than the few I mentioned, and I wouldn’t assume that just because the UA slips through that it has the same feature set. As for CometBird, I didn’t say it passed or not, only that browsers like it can be troublesome.

    > Looks to me like we’re in good shape: low-to-no false positives (vs. the deployed bulk of browsers) and fast paths most places.

    I’m skeptical, as you expand/fix your handful of sniffs to more browsers and versions I could totally see things like IE compatibility modes and mobile browsers causing headaches.

  21. Posted February 4, 2011 at 1:04 am | Permalink

    Hi Alex,

    Thanks for this article, you’re giving a well-researched voice to concerns I’ve had for a long time with feature detection.

    The worst thing that I’ve seen so far, and that has haunted us in Prototype.js for a while, is that if you test for on IE when Java is not installed, a dialog box pops up asking if you want to install Java. That’s not even measurable in performance terms, as it’s a complete disruption.

    While I hope that browser vendors refrain from this in the future, you never know when feature detection might cause similiar behavior, trigger a bug, etc. It’s extra code that has to be executed, and by definition, sometimes it’s really “tricky” code, because is testing something that’s not there. Plus, you run into the issue of the false positives, etc., etc.

    I really like your approach of pre-cached results with feature tests of a fallback. Awesome idea.

  22. Posted February 4, 2011 at 2:05 am | Permalink

    Hey John-David:

    We still don’t have a collision that indicates any test that should fail would fail. I’m totally willing to concede that there’s some risk here — as there is in feature testing — and that we should mitigate as far as possible. I didn’t outline other possible approaches as they’re not as easy to read in code or as terse, but using hashes of the UA string is one possible alternative for even tighter checks.

    As for IE compat modes, we should find out! Data is the best baseline for any of these conversations.

  23. Posted February 4, 2011 at 9:01 am | Permalink

    > Data is the best baseline for any of these conversations.

    I want to hope so, but as this series of posts was based on incorrect measurements, from word of mouth instead of your own tests, compounded with improper usage of has.js, and rushed/incorrect regexps it’s not looking so great.

    If used correctly there is no performance concern and no reason to inject UA strings, and the added uncertainty they bring, into the mix. However, if you find a better/faster way to perform a specific feature test please submit a patch.

  24. Posted February 4, 2011 at 10:23 am | Permalink

    Feature testing is expensive when it does touch the DOM.

    Would be nice if there was a Feature API/Spec. So that instead of has.js, or UA sniffing, you’d have to just (for example) Browser.has(‘html5:video’).

    This is an easy flag for the browsers to enable or disable when they’ve released new features and it’s easy for JS developers to just check. Again the cost is precomputed prior to loading.

  25. Posted February 4, 2011 at 10:29 am | Permalink

    Alex,

    Fascinating post. UA is not the answer – what you really need is real time DEV CAP (device capability). We’re getting ready to release an Android Browser that allows you to now only interact with the device but also gather real time HTTP traffic performance stats from inside the browser. Here’s a sample of what the data will include: http://www.5o9mm.com/har/viewer/v.pl?path=accounts/5o9/android-02-04-2011-18-27-55-GMT-infrequently-org-2011-02-on-performance-innumeracy-false-positives.har

    This is your blog post accessed from an Android device – you can see some of the dev cap info, carrier and also real time geo location in addition to the page elements.

    The full release will include more detailed information and also support a JavaScript Mobile Performance library that will allow for automated performance testing.

    Cheers,

    Peter Cranstone
    co-inventor of mod_gzip

  26. Posted February 4, 2011 at 12:15 pm | Permalink

    Hey Peter:

    I’m all for things that’ll let you test/cache faster! Hope this post didn’t come across as a “you must do it this way” sort of thing. It was meant as a generic case for caching and for doing less work when you know you don’t have to do it. Excited to see how your browser does!

    Regards

  27. Posted February 4, 2011 at 1:50 pm | Permalink

    How about this solution that is both fast and does not do UA sniffing.

    Have the feature detection library become a few character boot strapping JS plus an iframe that loads a container page with the actual detection library lined in. Then cache the results of the detection in localStorage.

    While this can only be implemented with high performance on modern browsers (postMessage being most important), luckily most mobile browsers belong to that category.

    That said I agree that ua based caching makes sense here. I discussed has.js with Peter Higgins last September and it was my impression that his plan was to actually built in such a mechanism (then again this was in Amsterdam, so :)

    Cheers
    Malte

  28. Eric True
    Posted February 5, 2011 at 12:27 am | Permalink

    How expensive is the regex matching? You may want to strategically order them so that the tests for presumed slower browsers happen first, and stop performing matches once you have the information you need.

    Excellent points in this post. Thanks.

  29. Nikolai Onken
    Posted February 6, 2011 at 2:10 am | Permalink

    One thing which bugs me is that it seems as if performance is the most important concern when talking pro/contra feature detection.
    Doesn’t performance mean that it always can be improved by some intelligent caching, be it on the client or on the server?

    Isn’t there much more to it? Shouldn’t we aim to write clean code free of branching which isn’t really needed? Shouldn’t we be looking for ways to distribute code onto different types of devices? We’re talking mobile, but really, look at the numbers: tons of tablets expected this year with different resolutions and input paradigms, TVs having embedded browsers, even cars running a browser dashboard!

    I want to write stuff for these environments and for me, shipping down the wire everything feels completely wrong (I know I am targeting a TV and I even know which one). We should be careful being stuck with old models and automatically applying them to new contexts. JavaScript is in much wider use now than that we should blindly follow old patterns.. Then again it completely depends on context, if you target traditional websites and high end phones, maybe feature testing is exactly the right thing :) Just don’t jump into it too fast!

  30. Dave Chapman
    Posted February 9, 2011 at 6:33 am | Permalink

    Hi Alex,

    I’ve been struggling for sometime with the whole UA detection/feature detection/object inference…

    For instance GWT (Google Web Toolkit, for those not in the know) uses a combination of UA detection and object inference for it’s deferred binding mechanism.

    if (ua.indexOf(“opera”) != -1) {
    return “opera”;
    } else if (ua.indexOf(“webkit”) != -1) {
    return “safari”;
    } else if (ua.indexOf(“msie”) != -1) {
    if (document.documentMode >= 8) {
    return “ie8″;
    } else {
    ….

    I asked why they chose to parse the ua string, which can lie e.g.

    1. Certain addons to IE alter/break the ua string (I can’t recall which) but I’ve seen server logs with “…MSIE 6; MSIE 7;…” in the ua string
    2. Opera (historically) allows the user to switch the ua string
    3. Sometimes browsers report the wrong string e.g. Maxthon installed over IE 6 reported an IE 7 ua string
    4. Currently no browser adheres to the standard for UA strings (e.g. product/version see RFC 2616) they would all be Mozilla 4 (or 5)

    I got no answer (from Google).

    I realise that using Object inference to determine the browser/version can sometimes be broken by client side code but personally I’ve found it to be more robust than using the UA string.

    Just my 2p. :o)

    Cheers,
    Dave

  31. Adrian Schmidt
    Posted February 16, 2011 at 7:33 am | Permalink

    Another great post. Nice to see some expanding on the thoughts in the last one.

    Seems like a lot of people had an easier time understanding your intentions this time around too :)

    I think Faruk has let himself down by being a crybaby. No offense, but I had to say it. And concerning his post “Lest we forget”; I just assume that stuff like this would be made available through libraries, just like feature tests are. So people like me, who are not experts on these subjects (at least not yet ;) ) will be able to use it just as safely. So why go on crying about that us ‘regular’ people aren’t competent enough to use these techniques?

    Sorry for the rant. Great post, again :)

    /Ad

    PS: Your Chrome and Safari if-statements check for Firefox:
    if (ua.search(FF36) == 0)

  32. Posted February 18, 2011 at 9:33 am | Permalink

    As a continuation of Alex’s post I have created a series of short screencasts examining the cost of feature testing and pre DOM load reflow.

  33. Posted March 5, 2011 at 11:55 pm | Permalink

    There’s a simple solution here. Use Conditional Comments or similar IE-specific hacks to target the older browser versions we all complain about. Then only feature detect to find browser-specific differences between IE 9 and other browsers.

    Problem solved. HTML5’s tagline should be: Using yesterday’s technologies, today. (I can’t tell you how often I’ve thanked Microsoft for VML, given its similarities to SVG. Sure it’d be nice not to use it, but I’m glad it’s in IE6 even so…)

2 Trackbacks

  1. […] for clarification (is he saying user agent sniffing is more preferable?), which resulted in a second post with numbers to back up his position, along with more clarification, where he seems to be […]

  2. […] | # | 0 In this series of screencasts I present my response to Alex Russell’s recent blog posts over the cost of feature […]