talk-long-life-web-apps



talk-long-life-web-apps

0 1


talk-long-life-web-apps

A talk at jQuery UK 2014

On Github matthew-andrews / talk-long-life-web-apps

- @andrewsmatt, mattandre.ws

Long Life Web Apps

Our battle against the browser

by Matt Andrews, FT Labs (mattandre.ws / @andrewsmatt)

Good hacks and great performance, smart multivariate responsiveness, and web apps that deserve far more than the lowly title of ‘page’.

Our web apps

} 'Long life' web app?
  • Single page app
  • 'Many page app' - website?*
  • Cross platform - 1 codebase for all
  • Offline first
  • Time on page very long (up to days)**
  • Once installed, code on device forever***

* Economist only. FT App, soon.

** On platforms that can run web apps in the background (Windows, Blackberry)

*** Well, until clear caches.

I'm a developer on the FT Web app, which is a single page web app that works offline across all major mobile and desktop browsers - creating a full-screen native-app like experience without having to be distributed through any app stores. I'm going to talk through some of the challenges we faced when building this sort of website.

Now need to think much more about:

  • Memory management and performance
  • Handling link clicks
  • Caching
  • Backwards compatibility
As a web developer it's really easy to forget how powerful the browser actually is - and how much work it does for you. Unlike traditional software development when you make a webpage you normally don't need to worry about memory management or garbage collection or backwards compatability - even if you do break something, most mistakes can be fixed by the user simply clicking refresh.

Memory Management & Performance

OK so this isn't really an issue that is new to this class of website - but our websites run for hours or for packaged apps - sometimes even days - so performance is critical. The browser has a very good garbage collector - and when you're making normal webpages you probably never need to think about it. But when you have long lived pages - with user session times reaching dozens of minutes or longer, it can cause a degraded experience - and we suspect it is the cause of some of the crashes our application experiences.

Detached DOM

- Some javascript libraries, such as jQuery, keep a cache of DOM nodes - so if you're using jQuery and want to remove an element from the page you *must* use jQuery's APIs otherwise that cache won't know it needs to update and you'll get a memory leak. - But it's easy to end up with this sort of memory leak in application code too - espeically in apps where you're constantly building and tearing down pages. The first version of the FT web app was riddled with them. - Luckily Chrome Dev Tools have some quite powerful and relatively easy to use tools for detecting them.

How to find it

  • Chrome DevTools » Profiles » Heap Snapshot
  • Filter by detached to see the node.
  • Hover the results to get clues about which objects have become detached
  • ProTip: Profile; do one action; Profile again - and use the comparison view (filtered by 'detached') to see if that action caused a leak

Avoid Memory Leaks

  • Tidy up after yourself - eg. addEventListener » removeEventListener
  • Check 3rd party components tidy up after themselves on destroy
  • Be careful with jQuery - it keeps caches to DOM nodes.Don't mix jQuery DOM methods with native browser methods / libs.
  • Minimise the number of objects added to global scope.

Fix memory leaks to

  • increase stability
  • allow the garbage collector to do its job
  • but for performance, our problems are usually with rendering

Using Timeline

Feel like it's just telling me I'm doing everything wrong

I want a timeline that looks like this:

Don’t use your mouse when Timeline profiling. Give elements IDs and trigger the events on them that you want to profile via the console

(Update: Read more about removing noise from timeline)

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.

Nobody writes code like that

(Update: Read more about Fixing Layout Thrashing)

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

Recap

  • Use heap profiler to uncover memory leaks
  • Batch DOM read and writes to avoid layout thrashing

… but don't take my word on any of this. Measure for your application.

Handling Links in a single page app

Should be easy, right?

The Challenge

When a user clicks on a link on a full screen iOS web app:

  • Your web app closes « Better this if didn't happen
  • The link opens in Safari

Handling links?

  • Listen to clicks, and preventDefault()
  • Update the URL bar: history.pushState()
  • Load/display the new page.
  • Go back with history.back()

'After going back I wasn't where I was before I clicked the link'*

  • Listen to clicks, and preventDefault()
  • Save scroll position for current page
  • Update the URL bar: history.pushState()
  • Load/display the new page
  • Restore scroll position if appropriate
  • Go back with history.back()

* Same is true for web forms

- Even simple things like when you click a link, the browser will wipe the slate clean and the next page will be loaded fresh (it will even remember details like how far down they have scrolled on the page they clicked from so that if the user clicks back it will restore their scroll position). When you create a single page app and start relieving the browser of the responsibility of things as simple as 'handling links' - all those little things the browser does for you become your responsibility.

iOS 7 is completely broken

… managing states through hash or other techniquethe history object will not keep your navigation history… history.back() will never work

http://mobilexweb.com/blog/safari-ios7-html5-problems-apis-review

And again for iOS 7

  • Listen to clicks, and preventDefault()
  • Save scroll position for current page
  • Update the URL bar: history.pushState()
  • Or on iOS 7 push the new URL into a custom history array
  • Load/display the new page.
  • Or on iOS 7 pop the last item from the custom history array & load it
  • Restore scroll position if appropriate
  • Go back with history.back()

…until a journalist embeds a Tweet

  • Twitter library listens to all clicks on host page
  • If URL matches twitter.com/intent it opens in a new window(Reminder: on iOS this will close your web app)
  • (Also an issue on packaged apps - eg. Windows 8 Web Apps, Chrome packaged apps, hybrid apps)
  • (Update: Read more about embedding Tweets in single page apps)

I'm being unfair to Twitter, lots of libraries have problems.

  • Some ad libs wait for DOMContentReady - usually happened a long time ago
  • document.write isn't going to work.
  • Analytics libraries lose data when device is offline
If you’re building websites in non-standard ways (full screen ‘web apps’; packaged/hybrid apps; single page apps and/or offline first apps) don’t automatically assume that because you’re using ‘web technologies’ you will be able to use every existing library that was built for the web. All libraries – even modern, well written ones like the one Twitter use for embedding Tweets – are built with certain assumptions that may not be true for your product.
"It won't work in the web app" --Everyone at the FT But we must be doing something wrong if this is the most popular expression in editorial.

3rd party libraries: solutions?

Well no, not really :(

There isn't really a good solution right now. The fundamental problem is that the web lacks encapsulation.

Recap

  • Immersive full-screen experience is possible, but very buggy
  • When going back, restore scroll position and form data
  • Thoroughly test 3rd party libraries
  • Allocate time around each iOS launch to fix what Apple break

Caching for offline

Naive approach

  • if device is online, load from the web
  • else load from cache
  • Browser even has handy method for checking connection: navigator.onLine

navigator.onLine doesn't work

  • Flaky connections
  • Captive portals
  • Good connection but your server is down or blocked

Fun fact:

On Desktop Firefox navigator.onLineis only false when File » Work Offline is ticked

Who even uses that anyway?

Live with uncertainty

  • Assume the device is offline, load from cache
  • Then try the network for new data in the background

For a page to load offline,you need to use the HTML5 Application Cache

Manifest looks like this:

CACHE MANIFEST
# 2014-05 v1
/lib/fonts/BentonSansBold.ttf
/lib/img/startupscreen/splash-logo.png

NETWORK
*

And to use it you need to add an attribute to <html>

<DOCTYPE html>
<html manifest="mywebapp.manifest">

Sadly, it's not quite that easy

  • Leaks storage (workarounds exist)
  • Inflexible and unintuitive API
  • Makes browser susceptible to scary man in the middle attacks… whether you use it or not
  • Will soon be replaced by Service Worker
Technically already available in Canary, behind a flag - but you can only install, uninstall and send messages to it right now - all the cool offline stuff not ready yet
Even Chrome dev relations people will tell you the Application Cache is a douchebag.

But…

  • It does work
  • across 80%+* of browsers (iOS, Android, Chrome, Opera, IE10+)
  • Only Chrome and Firefox have committed to building Service Worker implementations

* http://caniuse.com/offline-apps

AppCache OK

  • Yes it's ugly for developers
  • But it enables amazing experiences for users
  • There are use cases where it works well
  • Unfortunately, building a newspaper app isn't one of them

Backwards compatibility

Unlike native apps,offline websites only download updates

when they're open

and only applies those updates on refresh

And some users get stuck on old versions - we're still detecting API callsfrom Javascript that hasn't been served for a year

App start events split by version of client side code

Guidance

  • Expect some users to get stuck, add tools to help users recover their apps
  • Test different versions of client side code against different backend versions
  • Add monitoring
  • Add buttons to recovery tools that appear after a CSS animation if JS fails
  • Version everything - API endpoints, data formats
  • Client side database migrations are painful. Store data in its simplest form (JSON better than HTML)

Summary

  • Browsers do much more than render. If you're website doesn't restore state well (scroll, form data) users will be upset
  • Offline is possible today, with enough persistence
  • Test, measure and monitor everything, including 3rd parties
  • Never listen to people who tell you not to use any particular technology

So don't listen to me

That's it!

@andrewsmatt

Offline Web Workshop

I'm running a workshop at SmashingConf onthe Offline Web in September in Freiberg