Conquering the uncanny valley

Techniques for matching native app performance on the web with HTML5

by Andrew Betts, FT Labs ( / @triblondon)

The uncanny valley

We've been doing this for a while


  • Works offline
  • Portable
  • Long battery life
  • Can be read in bright sunlight
  • Cheap
  • Ubiquitous
  • Bookmarkable
  • Sharable
  • Fast start up
  • Supports clipping/saving
  • Can be read in the dark
  • Updates in real time
  • Electronic delivery
  • Search
  • Personalisation
  • Deep linking
We need to care about supporting existing features as much as getting new ones

Lose the constraints

  • Tablet
  • Phone
  • Desktop
  • Laptop
  • TV
  • Games console
  • In car screen
  • In flight screen
  • Billboard
  • Kiosk
  • e-ink reader
  • Hot air balloon

Smooth transitions


No pauses


Matching expectations


HTTP requests are painful, so do fewer of them and make them count.

The basics

  • Async everything: defer and async on script tags
  • Even async scripts can block the load event:
    Start loading your scripts after onload
  • Reduce HTTP requests with spriting and script concatenation

Power yum yum

Source: Qualcomm

Latency by radio state

 Chrome, MacOS,
Velocity Conf wifi
Safari, iOS,
T-Mobile EDGE
1s intervals180ms850ms
10s intervals180ms1500ms
20s intervals180ms2100ms

Try it on the JSFiddle.


  • Aggressive batching - collect requests asynchronously:
    api.add('getStory', {'path': '/1'}, callback1);
    api.add('getStory', {'path': '/1'}, callback2);
    api.add('healthcheck', params, callback4);
  • Callbacks per action and per group
  • Process queues in the background


  • Makes this unnecessary – in theory, but:
    • Can't optimise cross-origin (third party scripts)
  • Still use cases for delayed requests:
    • Offline persistence of queued requests


  • Typically 70-95% of web page data is images
  • Use an accessible responsive images technique
  • Consider high resolution, high compression for high DPI screens

Third party scripts

  • One of the biggest users of polling timers, especially analytics
  • Test your performance before and after
  • Ask the right questions before you add a third party script

Live demo (ooo err)

Interested in helping me with this? Look at the code and drop us a line.

Prefetch and friends

Prefetch and friends

Pre-resolve DNS hostnames for assets later in the page:

<link rel='dns-prefetch' href=''>

Fetch subresources early so they're already there when needed:

<link rel='subresource' href='/path/to/some/script.js'>

Pre-fetch resources for likely future navgiations:

<link rel='prefetch' href='/most/likely/next/page.html'>

Pre-render an entire page in the background (Chrome only)

<link rel='prerender' href='/overwhelmingly/likely/next/page.html'>


Layout, paints, animation frames and 'jank' coming up.

Use the tools

  • Timeline
  • Chrome tracing
  • FPS meter

Old flexbox

IE <11, Firefox (current), Safari <7, iOS <7, Android (current), Blackberry <10

New flexbox

IE 11+, Chrome, iOS 7+, Blackberry 10+

Hover effects

Disable hover effects on non-hoverable devices and during scrolls

.hoverable a:hover { ... }

<body class='hoverable'>

Live demo (ooo err)

Frame rates

Activate meter in chrome://flags

No border-radius


Layout boundary

The area the browser has to re-layout when you change something

For more info see Wilson Page's post or Boundarizr by Paul Lewis.

Layout boundary (fixed)

Layout 'thrashing'

  • DOM operations are synchronous but 'lazy' by default
  • Browser will batch writes for you
  • But you force it to write if you try to read something

Interleaved reads/writes

var h1 = element1.clientHeight;              <== Read (measures the element) = (h1 * 2) + 'px';     <== Write (invalidates current layout)
var h2 = element2.clientHeight;              <== Read (measure again, so must trigger layout) = (h1 * 2) + 'px';     <== Write (invalidates current layout)
var h3 = element3.clientHeight;              <== Read (measure again, so must trigger layout) = (h3 * 2) + 'px';     <== Write (invalidates current layout)

Batching reads/writes manually

var h1 = element1.clientHeight;              <== Read
var h2 = element2.clientHeight;              <== Read
var h3 = element3.clientHeight;              <== Read = (h1 * 2) + 'px';     <== Write (invalidates current layout) = (h1 * 2) + 'px';     <== Write (layout already invalidated) = (h3 * 2) + 'px';     <== Write (layout already invalidated)
h3 = element3.clientHeight                   <== Read (triggers layout)

Asynchronous DOM?

Use Wilson's FastDOM library to get asynchronous DOM today. {
  var h1 = element1.clientHeight;
  fastdom.write(function() { = (h1 * 2) + 'px';
}); {
  var h2 = element2.clientHeight;
  fastdom.write(function() { = (h1 * 2) + 'px';

This works by using requestAnimationFrame to batch writes

Live demo (ooo err)


Image decoding

Image decoding is probably the most expensive
thing you ask the browser to do when your page loads.

  • Don't load on demand (battery killer on mobile)
  • Load but don't decode? No native API (yet. Ahem)
  • Polyfill by downloading data: URIs
    • Base64-encode on server
    • Download with XHR
    • Insert string into <img src=''> to trigger decoding
  • Con: Fools the browser, may mean you lose browser level optimisations

Hardware accelerated transforms

You need the GPU if you're going to animate a

move, scale
filter, rotate

Using accelerated animations

  • Repositioning elements causes a relayout
  • Accelerated animation = paint only
  • First: move element to GPU layer
    .thing { -webkit-transform: translateZ(0); }
  • Then: apply a transition or animation
    .thing { -webkit-transition: all 3s ease-out; }
    .thing.left { -webkit-transform: translate3D(0px,0,0); }
    .thing.right { -webkit-transform: translate3D(600px,0,0); }

This will-change

.thing { will-change: transform, opacity }

Storing data

HTML5 can store data on device too, it's just... well, it's complicated.

A happy family of technologies

  • Cookies
  • localStorage (and sessionStorage)
  • WebSQL
  • IndexedDB
  • HTML5 Application Cache
  • Files API
  • Cache API

localStorage vs IndexedDB

HTML5 Application cache

If you really want to know more, go read this and watch this

AppCache is dead, long live


It's like a server in the browser

Storage optimisation

While we are limited by tiny quotas, we need to learn to live with less.

Text encoding 'compression'

  • JavaScript internally uses UTF-16 for text encoding
  • Great idea for processing: fast string operations, full support for Unicode BMP
  • Terrible idea for storage of English text or base-64 encoded images.

Squeeze your bits

Text S i m p l e
Decimal 83 105 109 112 108 101
As binary 01010011 01101001 01101101 01110000 01101100 01100101
Shifted 01010011 01101001 01101101 01110000 01101100 01100101
As hex 53 69 6D 70 6C 65

ASCII packed into UTF-16

function compress(s) {
    var i, l, out='';
    if (s.length % 2 !== 0) s += ' ';
    for (i = 0, l = s.length; i < l; i+=2) {
        out += String.fromCharCode((s.charCodeAt(i)<<8) + s.charCodeAt(i+1));
    return out;


function decompress_v1(data) {
    var i, l, n, m, out = '';
    for (i = 0, l = data.length; i < l; i++) {
        n = data.charCodeAt(i);
        m = Math.floor(n / 256);
        out += String.fromCharCode(m) + String.fromCharCode(n % 256);
    return out;

Click delay

More click, less wait.


  • Removes 300ms delay on touch
  • Programmatic clicks aren't quite the same - we handle it where we can (eg apply focus)
  • Fixed in Chrome 32 (Stable), Firefox 30 (Nightly). Still an issue in Safari & IE

Live demo (ooo err)


With fastclick

Note: This demo shows the effect of Fastclick without using Fastclick itself


When you can't make it any faster...
make it seem faster.

That's it!

Contact me:

For jobs in Beijing, visit

For jobs in London, visit