Progressive Web Apps Across All Frameworks

Progressive Web Apps Across All Frameworks

Progressive Web Apps can be built using any JavaScript library or framework, whether it's React, Angular 2.0, Ember or Polymer. In this talk, we'll dive into how to craft offline, lightning fast web apps using each of these solutions. Learn how to take advantage of Service Workers, Server-side Rendering and an application "shell" architecture to optimise for first meaningful paint, fast-first load and repeat visits. We'll also cover techniques for progressive-enhancement using these libraries so your users still get the best user experience supported by their browser.

📹 Video: https://www.youtube.com/watch?v=srdKq0DckXQ&list=PLNYkxOF6rcIDz1TzmmMRBC-kd8zPRTQIP&index=21

96270e4c3e5e9806cf7245475c00b275?s=128

Addy Osmani

May 20, 2016
Tweet

Transcript

  1. Progressive Web Apps Across All Frameworks + AddyOsmani @addyosmani

  2. None
  3. Built with a JavaScript framework / library Existing apps Progressively

    Enhanced
  4. Web App install banner Splash screen Offline support

  5. None
  6. ?

  7. Web App install banner Splash screen Offline support

  8. Add to home screen Offline support Web App install banner

  9. Mobile Safari on iOS

  10. ?

  11. 5miles FlipKart 70% BETTER CONVERSIONS A2HS USERS HAD 30% BETTER

    CONVERSIONS A2HS USERS HAD
  12. selioapp.com 10x 6+ cheaper than native minutes USER ACQUISITION AVERAGE

    SESSION Client-side JavaScript Server-side JavaScript
  13. None
  14. None
  15. None
  16. None
  17. None
  18. INSTANT

  19. Time to first paint

  20. Time to first meaningful paint

  21. Time to first meaningful interaction

  22. None
  23. None
  24. navigator.serviceWorker .register('sw.js',{ scope: ‘./' }) .then((registration) => { // Service

    Worker registered });
  25. App Shell App Shell App Shell Index About Contact App

    Shell Application Shell Content I swear I can hear Google sigh every time I start typing in the search bar. Google Earth: useful for checking which yards have milkshakes. Just ran a 10K (Ice cream truck wouldn't stop)
  26. App Shell Inline critical CSS in <head> Async load CSS/JS

    for current view App Shell I swear I can hear Google sigh every time I start typing in the search bar. Google Earth: useful for checking which yards have milkshakes. Just ran a 10K (Ice cream truck wouldn't stop) .toolbar { } .card { } .drawer { } <script async> loadCSS
  27. None
  28. None
  29. var CommentBox = React.createClass({ getInitialState: function() { return {data: []};

    }, render: function() { return ( <div className="commentBox"> <h1>Comments</h1> <CommentList data={this.state.data} /> <CommentForm /> </div> ); } });
  30. Hacker News new | comments | show | ask |

    jobs | submit 1. Write code nobody else can read 2. StackOverflow copy/pasting as a service 3. You might not need 15 package managers
  31. None
  32. 2014

  33. None
  34. None
  35. Responsive Web App Manifest Service Worker Content caching Universal rendering

    1 2 3 4 5
  36. None
  37. Responsive Web App Manifest Service Worker App Shell Content caching

    Universal rendering 1 2 3 4 5
  38. React HN

  39. { "name": "React HN", "short_name": "React HN", "icons": [{ "src":

    "img/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" },...], "start_url": "index.html", "background_color": "#4CC1FC", "display": "standalone", "theme_color": "#222222" }
  40. Responsive Web App Manifest Service Worker App Shell Content caching

    Universal rendering 1 2 3 4 5
  41. None
  42. Command-line "scripts": { "build": "cp node_modules/sw-toolbox/sw-toolbox.js && nwb build &&

    npm run precache", "precache": "sw-precache --root=public --config=sw-precache-config.json", ... }, package.json
  43. { "importScripts": [ "sw-toolbox.js", "runtime-caching.js" ], "stripPrefix": "public/", "verbose": true,

    "staticFileGlobs": [ "app/css/**.css", "app/**.html", "app/images/**.*", "app/js/**.js" ], } sw-precache-config.json
  44. (function(global) { 'use strict’ global.toolbox.router.get('/(.*)', global.toolbox.fastest, { origin: /\.(?:googleapis|gstatic)\.com$/ });

    global.toolbox.router.get('/(.+)', global.toolbox.fastest, { origin: 'https://hacker-news.firebaseio.com' }); })(self) runtime-caching.js
  45. <div id="app"></div> <script src="build/vendor.js"></script> <script src="build/app.js"></script> <script> if ('serviceWorker' in

    navigator) { navigator.serviceWorker.register('./service-worker.js', { scope: './' }) .then(function(registration) { registration.onupdatefound = function() { if (navigator.serviceWorker.controller) { var installingWorker = registration.installing; installingWorker.onstatechange = function() { switch (installingWorker.state) { case 'installed': break; case 'redundant': index.html
  46. None
  47. Go offline. App shell cached! Current session data.

  48. BUT…relaunch and…

  49. Responsive Web App Manifest Service Worker App Shell Content caching

    Universal rendering 1 2 3 4 5
  50. None
  51. INDEXED DB LOCALFORAGE DEXIE IDB

  52. var Firebase = require('firebase') var api = new Firebase('https://hacker-news.firebaseio.com/ v0')

    function fetchItem(id, cb) { itemRef(id).once('value', function(snapshot) { cb(snapshot.val()) }) } function fetchItems(ids, cb) { … } } function storiesRef(path) { return api.child(path) } HNService.js
  53. Text

  54. After loading just one page of comments.

  55. importScripts('./localforage.js') onmessage = (e) => { const batch = e.data;

    // Write batch to Indexed DB } …Web Workers? INDEXED DB
  56. REST https://www.flickr.com/photos/oakleyoriginals/8544765375/

  57. require('isomorphic-fetch') var endPoint = 'https://hacker-news.firebaseio.com/v0' var options = { method:

    'GET', headers: { 'Accept': 'application/json' } } function itemRef(id) { return fetch(endPoint + '/item/' + id + '.json', options) } HNServiceRest.js https://hacker-news.firebaseio.com/v0/item/11643791.json
  58. None
  59. None
  60. Responsive Web App Manifest Service Worker App Shell Content caching

    Universal rendering 1 2 3 4 5
  61. None
  62. None
  63. None
  64. // server.js var express = require('express') var React = require('react')

    var renderToString = require('react-dom/server').renderToString var ReactRouter = require('react-router') require('babel/register') var routes = require('./src/routes') var app = express() app.set('view engine', 'ejs') app.set('views', process.cwd() + '/src/views') app.set('port', (process.env.PORT || 5000)) app.use(express.static('public')) app.get('*', function(req, res) { ReactRouter.match({ routes: routes, location: req.url }, function(err, redirectLocation, props) { React Router
  65. // Deserialise caches from sessionStorage. loadSession() { if (typeof window

    === 'undefined') return idCache = parseJSON(window.sessionStorage.idCache, {}) itemCache = parseJSON(window.sessionStorage.itemCache, {}) }, // Serialise caches to sessionStorage as JSON. saveSession() { if (typeof window === 'undefined') return window.sessionStorage.idCache = JSON.stringify(idCache) window.sessionStorage.itemCache = JSON.stringify(itemCache) } Globals guards
  66. var parseHost = (function() { var a = document.createElement('a') return

    function(url) { a.href = url var parts = a.hostname.split('.').slice(-3) if (parts[0] === 'www') { parts.shift() } return parts.join('.') } })() Minimise reliance on the DOM var url = require('url') var parseHost = function(uri) { var hostname = url.parse(uri).hostname var parts = hostname.split('.').slice(-3) if (parts[0] === 'www') { parts.shift() } return parts.join('.') }
  67. Performance Cable Speed Index 0 1000 2000 3000 4000 Original

    Service Worker Server-rendering SSR + SW Content caching 1,004 1,106 1,140 1,062 2,063
  68. None
  69. Angular 1.5?

  70. Add to home screen Splash screen Offline support

  71. Fast first paint of content (target value: 1,000ms) Speed Index

    Metric (target value: 1,000ms) Speed Index Metric for repeat visit (target value: 1,000ms) 2500ms 8432ms 4181ms https://cherry-app.appspot.com
  72. Angular Mobile Toolkit

  73. Angular CLI

  74. Command-line $ npm install -g angular-cli $ ng new webapp

    --mobile && cd webapp $ ng serve
  75. Create Project Install Deps Scaffold App Generate Service Worker Build

    step for AppShell Generate App Manifest Run Tests Preprocess CSS Build --mobile
  76. App Manifest

  77. { "name": "Progressive", "short_name": "Progressive", "icons": [{ "src": "/android-chrome-192x192.png", "sizes":

    "192x192", "type": "image/png", },...], "theme_color": "#000000", "background_color": "#e0e0e0", "start_url": "/index.html", "display": "standalone", "orientation": "portrait" }
  78. Application Shell

  79. Command-line $ npm install --save @angular2-material/toolbar $ npm install --save

    @angular2-material/core
  80. import { Component } from '@angular/core'; import { APP_SHELL_DIRECTIVES }

    from '@angular/app-shell'; import { MdToolbar } from '@angular2-material/toolbar'; @Component({ moduleId: module.id, selector: 'hello-mobile-app', template: ` <h1> {{title}} </h1> `, styles: [], directives: [APP_SHELL_DIRECTIVES, MdToolbar] }) export class HelloMobileAppComponent { title = 'hello-mobile works!'; } hello-mobile works!
  81. import { Component } from '@angular/core'; import { APP_SHELL_DIRECTIVES }

    from '@angular/app-shell'; import { MdToolbar } from '@angular2-material/toolbar'; @Component({ moduleId: module.id, selector: 'hello-mobile-app', template: ` <md-toolbar> {{title}} </md-toolbar> `, styles: [], directives: [APP_SHELL_DIRECTIVES, MdToolbar] }) export class HelloMobileAppComponent { title = 'Hello Mobile'; } Hello Mobile
  82. @Component({ moduleId: module.id, selector: 'hello-mobile-app', template: ` <md-toolbar> {{title}} </md-toolbar>

    <md-spinner *shellRender></md-spinner> <div *shellNoRender>App is Fully Rendered</div> `, directives: [APP_SHELL_DIRECTIVES, MdToolbar, MdSpinner] })
  83. shellRender shellNoRender

  84. SERVICE WORKER

  85. Command-line $ ng serve --prod $ ng build --prod Berry

    PWA
  86. "{ "group": { "app": { "url": { "/app-concat.js": { "hash":

    "2431d95f572a2a23ee6df7d619d2b68ad65f1084" }, "/favicon.ico": { "hash": "164f9754ba7b676197a4974992da8fc3d3606dbf" }, "/icons/android-chrome-144x144.png": { "hash": "2eb2986d6c5050612d99e6a0fb93815942c63b02" }, dist/ngsw-manifest.json
  87. Angular 2 Weather Fast first paint of content (target value:

    1,000ms) Speed Index Metric (target value: 1,000ms) Speed Index Metric repeat visit (target: 1,000ms) 1083ms 2271ms 599ms
  88. OFFLINE COMPILATION

  89. Template compilation Angular 1 (Browser) Angular 2 XHR Parser DOM

    NG1/JS Template Parser AST Source/JS NG2/JS Source NODE NODE FILE BROWSER BUILD
  90. Angular 2 Weather Fast first paint of content (target value:

    1,000ms) Speed Index Metric (target value: 1,000ms) Speed Index Metric repeat visit (target: 1,000ms) 479ms 1049ms 294ms
  91. Angular Universal

  92. server view displayed server response asset download client init client

    data paint client takes over initial page load
  93. server view displayed user events in uncanny valley? client takes

    over initial page load
  94. import {AppComponent} from "./app” import {expressEngine} from "@angular/universal” import *

    as express from "express” let server = express(); server.engine(".html”, expressEngine); server.get("/”, (req, res) => { res.render("index”, { directives: [ AppComponent ] }); }); server.listen(8080); server.js
  95. Performance https://ng2-weather-pwa.firebaseapp.com/ 3G Speed Index 0 750 1500 2250 3000

    Original Offline compilation Universal rendering Service Worker 294 638 1,049 2,640
  96. None
  97. const BlogPostComponent = Ember.Component.extend({ title: Ember.computed('params.[]', function(){ return this.get('params')[0]; }),

    body: Ember.computed('params.[]', function(){ return this.get('params')[1]; }) }); BlogPostComponent.reopenClass({ positionalParams: 'params' }); export default BlogPostComponent;
  98. In my opinion, the data for Angular and Ember flat

    out disqualify them for mobile use. Henrik Joreteg
  99. Load Time First Boot Repeat Boot FastBoot Engines String Loading

    Project Svelte Service Worker String Loading
  100. None
  101. None
  102. # Get access to the ember command-line runner $ npm

    install -g ember-cli bower # Run `new` with an app name to create a new project $ ember new my-new-app # Once generation is complete, launch the app $ cd my-new-app && ember server
  103. // ember-progressive-webapp/app/router.js import Ember from 'ember'; import config from './config/environment';

    const Router = Ember.Router.extend({ location: config.locationType }); Router.map(function() { this.route('art', function() { this.route('detail', { path: '/detail/:slug'}); }); this.route('film', function() { this.route('detail', { path: '/detail/:slug'}); }); this.route('photography'); this.route('design'); }); export default Router;
  104. [{ "title": "Tooling Up", "slug": "tooling-up", "primaryColor": "#5a7785", "secondaryColor": "#455a64",

    "image": "/images/tooling-up.svg" }, { "title": "Expressing Brand in Material", "slug": "expressing-brand-in-material", "primaryColor": "#202226", "secondaryColor": "#333", "image": "/images/expressing-brand.svg" }, ..
  105. None
  106. Web App Manifest Service Worker Server Rendering (FastBoot) Performance Optimisation

    1 2 3 4
  107. { "name": "Zuperkülblog", "short_name": "Zuperkülblog", "icons": [{ "src": "assets/images/touch/touch-icon-192x192.png", "sizes":

    "192x192", "type": "image/png" },...], "start_url": "/", "background_color": "#497886", "display": "standalone", "theme_color": "#FFFFFF" }
  108. None
  109. Web App Manifest Service Worker Server Rendering (FastBoot) Performance Optimisation

    1 2 3 4
  110. None
  111. Command-line $ ember install broccoli-serviceworker

  112. //app/config/environment.js ENV.serviceWorker = { enabled: true, debug: true, fallback: [

    '/online.html /offline.html' ], precacheURLs: ['/mystaticresource'], networkFirstURLs: [ '/api/todos' ], excludePaths: ['test.*', 'robots.txt'], includeRegistration: true, serviceWorkerFile: "service-worker.js", skipWaiting: true, swIncludeFiles: [ 'bower_components/pouchdb/dist/pouchdb.js' ] }; Minimal config
  113. toolbox.cacheFirst toolbox.fastest toolbox.networkFirst toolbox.cacheOnly toolbox.networkOnly sw-toolbox

  114. None
  115. Web App Manifest Service Worker Server Rendering (FastBoot) Performance Optimisation

    1 2 3 4
  116. network boundary Browser GET /my-app FastBoot initial render HTML +CSS

    JavaScript payload App GET /user.json JSON payload API server GET /posts.json GET /cats.json JSON payload UI Ready
  117. Command-line $ ember install ember-cli-fastboot $ ember fastboot —serve-assets #

    Visit your app at http://localhost:3000 $ ember fastboot --environment production
  118. // $ ember install ember-network import Ember from 'ember'; import

    fetch from 'ember-network/fetch'; import ENV from 'ember-progressive-webapp/config/environment'; export default Ember.Route.extend({ model() { return fetch(ENV.baseURL + '/data/art.json') .then(function(response) { return response.json(); }); }, serialize(model) { return { post_slug: '/art/detail/' + model.get('slug') }; } }); Universal fetch
  119. None
  120. None
  121. Works without JavaScript too.

  122. Web App Manifest Service Worker Server Rendering (FastBoot) Performance Optimisation

    1 2 3 4
  123. {{content-for "head"}} {{content-for 'critical-path-css'}} {{content-for "head-footer"}} </head> {{content-for "body"}} <script

    src="assets/vendor.js" defer></script> <script src="assets/ember-progressive-webapp.js" defer></script> {{content-for "body-footer"}} index.html App Shell CSS
  124. Command-line $ ember install ember-cli-inline-content

  125. var EmberApp = require('ember-cli/lib/broccoli/ember-app'); module.exports = function(defaults) { var app

    = new EmberApp(defaults, { // Add options here … // ember-cli-inline-content inlineContent: { 'critical-path-css': 'app/styles/app.css' } }); return app.toTree(); }; ember-cli-build.js Path to critical styles
  126. None
  127. Command-line # If using ember-cli-deploy $ ember install ember-cli-deploy-gzip

  128. {{content-for "head"}} {{content-for 'critical-path-css'}} {{content-for "head-footer"}} </head> {{content-for "body"}} <script

    src="assets/vendor.js" defer></script> <script src="assets/ember-progressive-webapp.js" defer></script> {{content-for "body-footer"}} index.html Deferred script execution
  129. Performance Cable Speed Index 0 1250 2500 3750 5000 Original

    FastBoot Optimisations Service Worker 414 972 1,304 2,884
  130. DO LESS lazy BE

  131. DESIGN FOR ENVIRONMENTS constrained

  132. GO PROGRESSIVE g.co/ProgressiveWebApps

  133. + AddyOsmani @addyosmani MANY DINOSAURS WERE HARMED IN THE MAKING

    OF THIS PRODUCTION. Thanks!