Conquering the uncanny valley
Techniques for matching native app performance on the web with HTML5
by Andrew Betts, FT Labs (trib.tv / @triblondon)
The uncanny valley
Robotics professor Masahiro Mori, 1970Uncanny valley teaches us not to try and be something we're not. This is often a problem experienced by web applications. Web apps that simply try to emulate native without balancing capabilities of tech with actual user needs likely to fall into uncanny valley. BUT, we think web is still the best way, going to explain briefly whyWe've been doing this for a while
The FT has actually been publishing content on mobile for 125 years. For most of that time we've used a technology we call a newspaper.
Newsprint
- 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
Might seem glib, but newsprint has a whole host of features that neither native apps, nor the web to date have been able to matchWe need to care about supporting existing features as much as getting new ones
And look ahead to new platforms, new ways that people will want to get our contentLose the constraints
- Tablet
- Phone
- Desktop
- Laptop
- TV
- Games console
- In car screen
- In flight screen
- Billboard
- Kiosk
- e-ink reader
- Hot air balloon
Obviously we don't think this is a viable way to read the FT, but it makes you think, doesn't it, about how the web might evolve and what we might come to use it for
So with these ambitions in mind, and thinking about the UX innovations achieved by native app developers, and the risks of uncanny valley effects, the FT web app demonstrates an evolving compromise. Balances great UX that adapts to your needs, with what we can do on the web today.For example, our economist app starts out assuming that you have a touchscreen, but if it detects a mouse cursor moving, we add hover effects, navigational aids and tune our click targets for use with a mouse.Network
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
Latency by radio state
Chrome, MacOS,Velocity Conf wifi
Safari, iOS,T-Mobile EDGE
1s intervals
180ms
850ms
10s intervals
180ms
1500ms
20s intervals
180ms
2100ms
Try it on the JSFiddle.
Batching
- Aggressive batching - collect requests asynchronously:
api.add('getStory', {'path': '/1'}, callback1);
api.add('getStory', {'path': '/1'}, callback2);
api.send(callback3);
api.add('healthcheck', params, callback4);
api.send(callback5);
- Callbacks per action and per group
- Process queues in the background
HTTP 2.0 / SPDY
- 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
Images
- 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)
Prefetch and friends
Pre-resolve DNS hostnames for assets later in the page:
<link rel='dns-prefetch' href='hostname-to-resolve.com'>
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'>
Rendering
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'>
...
</body>
Live demo (ooo err)
Frame rates
Eli Fidler from BlackBerry at Edge 2: 1px text-shadows are really expensive. Don't do that stuffLayout 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 '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)
element1.style.height = (h1 * 2) + 'px'; <== Write (invalidates current layout)
var h2 = element2.clientHeight; <== Read (measure again, so must trigger layout)
element2.style.height = (h1 * 2) + 'px'; <== Write (invalidates current layout)
var h3 = element3.clientHeight; <== Read (measure again, so must trigger layout)
element3.style.height = (h3 * 2) + 'px'; <== Write (invalidates current layout)
etc.
Batching reads/writes manually
var h1 = element1.clientHeight; <== Read
var h2 = element2.clientHeight; <== Read
var h3 = element3.clientHeight; <== Read
element1.style.height = (h1 * 2) + 'px'; <== Write (invalidates current layout)
element2.style.height = (h1 * 2) + 'px'; <== Write (layout already invalidated)
element3.style.height = (h3 * 2) + 'px'; <== Write (layout already invalidated)
h3 = element3.clientHeight <== Read (triggers layout)
etc.
Asynchronous DOM?
Use Wilson's FastDOM library to get asynchronous DOM today.
fastdom.read(function() {
var h1 = element1.clientHeight;
fastdom.write(function() {
element1.style.height = (h1 * 2) + 'px';
});
});
fastdom.read(function() {
var h2 = element2.clientHeight;
fastdom.write(function() {
element2.style.height = (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, scalefilter, 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); }
.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
AppCache is dead, long live
ServiceWorker
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
UTF-16
卩
浰
汥
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;
}
Decompressing
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.
Fastclick
- github.com/ftlabs/fastclick
- 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)
Note: This demo shows the effect of Fastclick without using Fastclick itself
Perception
When you can't make it any faster...make it seem faster.