Or Why We Still Have To Test In Every Browser, Web Standards Notwithstanding
It’s pretty seldom that anyone mentions web pages these days, other than in historical reference to days long gone by (yes, a whole few years ago). Web sites, sure, but not if what is really wanted is to replace something that, not so long ago, would have been some native code for a smartphone (or a little further back still, a desktop computer). Generally speaking, the most common term tripping from client’s lips these days is ‘web applications’ – or webapps, because who has time for spaces and proper spelling, amirite?
Of course, the client in question is almost certainly not interested in a webapp because they’re hoping to take advantage of the unique properties and capabilities of any particular browser (unless they’re looking for a line-of-business intranet application in IE8, in which case, you have both my scorn and my pity). Everyone wants their app to have the potential to reach the widest audience possible – and that means supporting all the mainstream browsers.
Oh, and all of the mainstream mobile devices, and their browsers; and maybe smart TVs, consoles, and I hear they’re coming out with, like, iWatches and stuff, can we target those too?
This is, after all, a major selling point of a web application – the promise of ‘write once, run anywhere’. One codebase, shared across all of your supported platforms. Since every project comes complete with a limited budget and timeframe, this can allow you to offer your product across a wider range of platforms than if you had to create a different version for each; and in addition to capturing the mainstream devices, your application may be able to appeal to users on minority platforms (Blackberry or Windows Phone, for example), that you couldn’t otherwise afford to support.
The dark side to this is that often, your client will expect that your webapp can and will support all of these various devices and browsers, and yet fail to understand why this should affect the project’s timeframe or cost. "Hold on," they’ll say, "I thought you said this webapp thing could support every browser that ever was, and more screen and device sizes than have been dreamt of in my philosophies?"
Well, perhaps I exaggerate; but the difficulties involved in supporting even just recent versions of browsers across many platforms tends to get underestimated by clients – and, sometimes, developers as well.
The Browser is the new Virtual Machine
Of course, the promise of ‘write once, run anywhere’ has been made before, most famously by Sun Microsystems for its Java language, and the Java Virtual Machine. Developers familiar with the language have an old joke based on the marketing slogan: "Write once, debug everywhere"; and the joke applies all the more certainly to web development.
While all major browsers follow the W3C standards, and carefully feature test against the specifications, their implementations are unique – at least Java developers could expect that the JVM was being made by engineers who, if nothing else, could step across the hallway and ask their fellows about the details of this or that feature implementation.
The developers for Firefox do their best to offer something like this, by developing entirely in open source; Google and Safari each have their closed components, though (and Blink and Webkit diverge by the day), and the implementation details of Internet Explorer are a mystery. The same challenges that faced developers working with the JVM on differing operating systems and/or hardware – subtle bugs encountered in surprising places, limited subsets of functionality available across all platforms (and sometimes, not available using the same mechanisms, or to the same degree) – are faced by webapp developers today.
Just look at this sample of the polyfills, shims and outright hacks collected to try and bridge the feature gaps between browsers, or offer a single sane interface over differing implementations.
Realistically, when estimating projects and committing to which browsers to support, each browser should be considered a separate platform with its attendant needs for platform-specific code and testing. Moreover, supporting any mobile device and browser greatly increases the chance of needing platform-specific code (not to mention the significant cost of ‘responsive design’ styling to make a site mobile-friendly). Major versions of said browsers may also qualify for this careful treatment (consider the vast diffference in features between IE8 and IE9, for example).
An Example for the skeptical
Any web developer whose had to work with multiple platforms will have horror stories about surprising issues – and not just those who’ve been working with cutting-edge tech (consider, for instance, the issues encountered when all you want to do is hide options in a select element).
But let’s take something both fairly cutting edge, and yet commonly requested, as our example: image manipulation on the client side. To cut this example down to the barebones, this is the scenario: we have the user select an image using a file input, that we the read in using FileReader, let the user decide how they want to crop the image, and perform the crop and create a new image using the Canvas. Fairly involved, but nothing any modern browser shouldn’t be able to do.
Ah, but are we including IE9 in our array of ‘modern browsers’ that we have to support? We probably are, since IE9 still has a 10% market share as of this writing; but IE9 doesn’t support FileReader, so we have to include some complex shim to add the functionality we need (probably Flash or Silverlight based).
Okay, but IE9 is old news. Truly modern browsers have everything we need. Oh? But what if we need to convert the resulting image into a blob, for sending to the server? On chrome, getting a blob from the canvas isn’t supported yet. So, we have to include a polyfill to handle this case manually:
/**
* Canvas toBlob shim, adapted with thanks from https://code.google.com/u/105701149099589407503/,
* from this chrome bug thread: https://code.google.com/p/chromium/issues/detail?id=67587
*/
(function(){
/**
* Convert a base64 image dataURL from a canvas element, to a blob.
* @param {function} callback
* @param {string} type
* @param {number} quality
* @param {base64=} dataURL The dataURL to use, rather than fetch from the canvas.
*/
function dataURLToBlob(callback, type, quality, dataURL){
dataURL = dataURL || this.toDataURL(type, quality);
var bin = atob(dataURL.split(',')[1]),
len = bin.length,
len32 = len >> 2,
a8 = new Uint8Array(len),
a32 = new Uint32Array(a8.buffer, 0, len32),
tailLength = len & 3;
for(var i=0, j=0; i < len32; i++)
{
a32[i] = bin.charCodeAt(j++) |
bin.charCodeAt(j++) << 8 |
bin.charCodeAt(j++) << 16 |
bin.charCodeAt(j++) << 24;
}
while(tailLength--)
{
a8[j] = bin.charCodeAt(j++);
}
callback(new Blob([a8], {'type': type || 'image/png'}));
}
if(HTMLCanvasElement && Object.defineProperty && !HTMLCanvasElement.prototype.toBlob)
{
Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob',
{
value:dataURLToBlob
});
}
})();
See here:
/**
* Canvas toBlob shim, adapted with thanks from https://code.google.com/u/105701149099589407503/,
* from this chrome bug thread: https://code.google.com/p/chromium/issues/detail?id=67587
*/
(function(){
/**
* Convert a base64 image dataURL from a canvas element, to a blob.
* @param {function} callback
* @param {string} type
* @param {number} quality
* @param {base64=} dataURL The dataURL to use, rather than fetch from the canvas.
*/
function dataURLToBlob(callback, type, quality, dataURL){
dataURL = dataURL || this.toDataURL(type, quality);
var bin = atob(dataURL.split(',')[1]),
len = bin.length,
len32 = len >> 2,
a8 = new Uint8Array(len),
a32 = new Uint32Array(a8.buffer, 0, len32),
tailLength = len & 3;
for(var i=0, j=0; i < len32; i++)
{
a32[i] = bin.charCodeAt(j++) |
bin.charCodeAt(j++) << 8 |
bin.charCodeAt(j++) << 16 |
bin.charCodeAt(j++) << 24;
}
while(tailLength--)
{
a8[j] = bin.charCodeAt(j++);
}
callback(new Blob([a8], {'type': type || 'image/png'}));
}
if(HTMLCanvasElement && Object.defineProperty && !HTMLCanvasElement.prototype.toBlob)
{
Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob',
{
value:dataURLToBlob
});
}
})();
Okay, but now we’re golden, right? But what if we have to support iOS? Oh my, there is a world of hurt awaiting you. You’ll need to handle subsampling and vertical squashing on megapixel images, exif orientation to correct the orientation of the image, and limitations on the size/resolution of the canvas.
Long story short: what works in one place is not guaranteed to work in another – at least, not without significant effort in some cases. "Write Once, Run Anywhere" is still just a pleasant dream.
So what does this mean to me?
The net effect is that, once the cost of testing and supporting multiple browser platforms is considered, the benefits of a shared codebase are somewhat reduced relative to native apps.
In all likelihood, it will still be faster and less expensive to create a webapp compared to a native application equivalent for each platform to be supported, assuming that platforms > 1, but it should not be expected that the difference will be drastic, especially if support of cutting-edge features is desired.
This is something that needs to be made clear to your client up-front – put simply, making a webapp is not easier than making a native application. There are advantages, but that isn’t one of them.