$30 off During Our Annual Pro Sale. View Details »

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

Addy Osmani

May 20, 2016
Tweet

More Decks by Addy Osmani

Other Decks in Programming

Transcript

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

    View Slide

  2. View Slide

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

    View Slide

  4. Web App install banner Splash screen Offline support

    View Slide

  5. View Slide

  6. ?

    View Slide

  7. Web App install banner Splash screen Offline support

    View Slide

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

    View Slide

  9. Mobile Safari on iOS

    View Slide

  10. ?

    View Slide

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

    View Slide

  12. selioapp.com
    10x
    6+
    cheaper than native
    minutes
    USER ACQUISITION
    AVERAGE SESSION
    Client-side
    JavaScript
    Server-side
    JavaScript

    View Slide

  13. View Slide

  14. View Slide

  15. View Slide

  16. View Slide

  17. View Slide

  18. INSTANT

    View Slide

  19. Time to first paint

    View Slide

  20. Time to first
    meaningful paint

    View Slide

  21. Time to first
    meaningful interaction

    View Slide

  22. View Slide

  23. View Slide

  24. navigator.serviceWorker
    .register('sw.js',{ scope: ‘./' })
    .then((registration) => {
    // Service Worker registered
    });

    View Slide

  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)

    View Slide

  26. App Shell
    Inline critical CSS in
    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 { }
    <br/>loadCSS<br/>

    View Slide

  27. View Slide

  28. View Slide

  29. var CommentBox = React.createClass({
    getInitialState: function() {
    return {data: []};
    },
    render: function() {
    return (

    Comments



    );
    }
    });

    View Slide

  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

    View Slide

  31. View Slide

  32. 2014

    View Slide

  33. View Slide

  34. View Slide

  35. Responsive
    Web App Manifest
    Service Worker
    Content caching
    Universal rendering
    1
    2
    3
    4
    5

    View Slide

  36. View Slide

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

    View Slide

  38. React HN

    View Slide

  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"
    }

    View Slide

  40. Responsive
    Web App Manifest
    Service Worker App Shell
    Content caching
    Universal rendering
    1
    2
    3
    4
    5

    View Slide

  41. View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide




  45. <br/>if ('serviceWorker' in navigator) {<br/>navigator.serviceWorker.register('./service-worker.js', {<br/>scope: './'<br/>})<br/>.then(function(registration) {<br/>registration.onupdatefound = function() {<br/>if (navigator.serviceWorker.controller) {<br/>var installingWorker = registration.installing;<br/>installingWorker.onstatechange = function() {<br/>switch (installingWorker.state) {<br/>case 'installed':<br/>break;<br/>case 'redundant':<br/>index.html<br/>

    View Slide

  46. View Slide

  47. Go offline. App shell cached!
    Current session data.

    View Slide

  48. BUT…relaunch and…

    View Slide

  49. Responsive
    Web App Manifest
    Service Worker App Shell
    Content caching
    Universal rendering
    1
    2
    3
    4
    5

    View Slide

  50. View Slide

  51. INDEXED DB
    LOCALFORAGE
    DEXIE
    IDB

    View Slide

  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

    View Slide

  53. Text

    View Slide

  54. After loading just one page of comments.

    View Slide

  55. importScripts('./localforage.js')
    onmessage = (e) => {
    const batch = e.data;
    // Write batch to Indexed DB
    }
    …Web Workers?
    INDEXED DB

    View Slide

  56. REST
    https://www.flickr.com/photos/oakleyoriginals/8544765375/

    View Slide

  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

    View Slide

  58. View Slide

  59. View Slide

  60. Responsive
    Web App Manifest
    Service Worker App Shell
    Content caching
    Universal rendering
    1
    2
    3
    4
    5

    View Slide

  61. View Slide

  62. View Slide

  63. View Slide

  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

    View Slide

  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

    View Slide

  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('.')
    }

    View Slide

  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

    View Slide

  68. View Slide

  69. Angular 1.5?

    View Slide

  70. Add to home screen Splash screen Offline support

    View Slide

  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

    View Slide

  72. Angular Mobile Toolkit

    View Slide

  73. Angular CLI

    View Slide

  74. Command-line
    $ npm install -g angular-cli
    $ ng new webapp --mobile && cd webapp
    $ ng serve

    View Slide

  75. Create
    Project
    Install Deps Scaffold App
    Generate
    Service Worker
    Build step for
    AppShell
    Generate
    App Manifest
    Run Tests
    Preprocess
    CSS
    Build
    --mobile

    View Slide

  76. App Manifest

    View Slide

  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"
    }

    View Slide

  78. Application Shell

    View Slide

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

    View Slide

  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: `

    {{title}}

    `,
    styles: [],
    directives: [APP_SHELL_DIRECTIVES, MdToolbar]
    })
    export class HelloMobileAppComponent {
    title = 'hello-mobile works!';
    }
    hello-mobile works!

    View Slide

  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: `

    {{title}}

    `,
    styles: [],
    directives: [APP_SHELL_DIRECTIVES, MdToolbar]
    })
    export class HelloMobileAppComponent {
    title = 'Hello Mobile';
    }
    Hello Mobile

    View Slide

  82. @Component({
    moduleId: module.id,
    selector: 'hello-mobile-app',
    template: `

    {{title}}


    App is Fully Rendered
    `,
    directives: [APP_SHELL_DIRECTIVES, MdToolbar, MdSpinner]
    })

    View Slide

  83. shellRender shellNoRender

    View Slide

  84. SERVICE
    WORKER

    View Slide

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

    View Slide

  86. "{
    "group": {
    "app": {
    "url": {
    "/app-concat.js": {
    "hash": "2431d95f572a2a23ee6df7d619d2b68ad65f1084"
    },
    "/favicon.ico": {
    "hash": "164f9754ba7b676197a4974992da8fc3d3606dbf"
    },
    "/icons/android-chrome-144x144.png": {
    "hash": "2eb2986d6c5050612d99e6a0fb93815942c63b02"
    },
    dist/ngsw-manifest.json

    View Slide

  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

    View Slide

  88. OFFLINE
    COMPILATION

    View Slide

  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

    View Slide

  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

    View Slide

  91. Angular Universal

    View Slide

  92. server view
    displayed
    server
    response
    asset
    download
    client
    init
    client
    data
    paint
    client takes
    over
    initial page load

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  96. View Slide

  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;

    View Slide

  98. In my opinion, the data for Angular
    and Ember flat out disqualify them
    for mobile use.
    Henrik Joreteg

    View Slide

  99. Load Time
    First Boot Repeat Boot
    FastBoot
    Engines
    String Loading
    Project Svelte
    Service Worker
    String Loading

    View Slide

  100. View Slide

  101. View Slide

  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

    View Slide

  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;

    View Slide

  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"
    }, ..

    View Slide

  105. View Slide

  106. Web App Manifest
    Service Worker
    Server Rendering (FastBoot)
    Performance Optimisation
    1
    2
    3
    4

    View Slide

  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"
    }

    View Slide

  108. View Slide

  109. Web App Manifest
    Service Worker
    Server Rendering (FastBoot)
    Performance Optimisation
    1
    2
    3
    4

    View Slide

  110. View Slide

  111. Command-line
    $ ember install broccoli-serviceworker

    View Slide

  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

    View Slide

  113. toolbox.cacheFirst
    toolbox.fastest
    toolbox.networkFirst
    toolbox.cacheOnly
    toolbox.networkOnly
    sw-toolbox

    View Slide

  114. View Slide

  115. Web App Manifest
    Service Worker
    Server Rendering (FastBoot)
    Performance Optimisation
    1
    2
    3
    4

    View Slide

  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

    View Slide

  117. Command-line
    $ ember install ember-cli-fastboot
    $ ember fastboot —serve-assets
    # Visit your app at http://localhost:3000
    $ ember fastboot --environment production

    View Slide

  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

    View Slide

  119. View Slide

  120. View Slide

  121. Works without JavaScript too.

    View Slide

  122. Web App Manifest
    Service Worker
    Server Rendering (FastBoot)
    Performance Optimisation
    1
    2
    3
    4

    View Slide

  123. {{content-for "head"}}
    {{content-for 'critical-path-css'}}
    {{content-for "head-footer"}}

    {{content-for "body"}}


    {{content-for "body-footer"}}
    index.html
    App Shell CSS

    View Slide

  124. Command-line
    $ ember install ember-cli-inline-content

    View Slide

  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

    View Slide

  126. View Slide

  127. Command-line
    # If using ember-cli-deploy
    $ ember install ember-cli-deploy-gzip

    View Slide

  128. {{content-for "head"}}
    {{content-for 'critical-path-css'}}
    {{content-for "head-footer"}}

    {{content-for "body"}}


    {{content-for "body-footer"}}
    index.html
    Deferred script
    execution

    View Slide

  129. Performance
    Cable
    Speed Index
    0
    1250
    2500
    3750
    5000
    Original FastBoot Optimisations Service Worker
    414
    972
    1,304
    2,884

    View Slide

  130. DO LESS
    lazy
    BE

    View Slide

  131. DESIGN FOR
    ENVIRONMENTS
    constrained

    View Slide

  132. GO PROGRESSIVE
    g.co/ProgressiveWebApps

    View Slide

  133. + AddyOsmani
    @addyosmani
    MANY DINOSAURS WERE
    HARMED IN THE MAKING
    OF THIS PRODUCTION.
    Thanks!

    View Slide