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. 2.
  2. 5.
  3. 6.

    ?

  4. 10.

    ?

  5. 12.

    selioapp.com 10x 6+ cheaper than native minutes USER ACQUISITION AVERAGE

    SESSION Client-side JavaScript Server-side JavaScript
  6. 13.
  7. 14.
  8. 15.
  9. 16.
  10. 17.
  11. 18.
  12. 22.
  13. 23.
  14. 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)
  15. 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
  16. 27.
  17. 28.
  18. 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> ); } });
  19. 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
  20. 31.
  21. 32.
  22. 33.
  23. 34.
  24. 36.
  25. 38.
  26. 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" }
  27. 41.
  28. 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
  29. 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
  30. 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
  31. 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
  32. 46.
  33. 50.
  34. 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
  35. 53.
  36. 55.

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

    // Write batch to Indexed DB } ā€¦Web Workers? INDEXED DB
  37. 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.ļ¬rebaseio.com/v0/item/11643791.json
  38. 58.
  39. 59.
  40. 61.
  41. 62.
  42. 63.
  43. 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
  44. 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
  45. 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('.') }
  46. 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
  47. 68.
  48. 71.

    Fast ļ¬rst 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
  49. 74.
  50. 75.

    Create Project Install Deps Scaffold App Generate Service Worker Build

    step for AppShell Generate App Manifest Run Tests Preprocess CSS Build --mobile
  51. 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" }
  52. 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!
  53. 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
  54. 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] })
  55. 86.

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

    "2431d95f572a2a23ee6df7d619d2b68ad65f1084" }, "/favicon.ico": { "hash": "164f9754ba7b676197a4974992da8fc3d3606dbf" }, "/icons/android-chrome-144x144.png": { "hash": "2eb2986d6c5050612d99e6a0fb93815942c63b02" }, dist/ngsw-manifest.json
  56. 87.

    Angular 2 Weather Fast ļ¬rst 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
  57. 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
  58. 90.

    Angular 2 Weather Fast ļ¬rst 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
  59. 92.

    server view displayed server response asset download client init client

    data paint client takes over initial page load
  60. 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
  61. 95.

    Performance https://ng2-weather-pwa.ļ¬rebaseapp.com/ 3G Speed Index 0 750 1500 2250 3000

    Original Offline compilation Universal rendering Service Worker 294 638 1,049 2,640
  62. 96.
  63. 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;
  64. 98.

    In my opinion, the data for Angular and Ember flat

    out disqualify them for mobile use. Henrik Joreteg
  65. 99.

    Load Time First Boot Repeat Boot FastBoot Engines String Loading

    Project Svelte Service Worker String Loading
  66. 100.
  67. 101.
  68. 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
  69. 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;
  70. 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" }, ..
  71. 105.
  72. 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" }
  73. 108.
  74. 110.
  75. 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 conļ¬g
  76. 114.
  77. 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
  78. 117.

    Command-line $ ember install ember-cli-fastboot $ ember fastboot ā€”serve-assets #

    Visit your app at http://localhost:3000 $ ember fastboot --environment production
  79. 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
  80. 119.
  81. 120.
  82. 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
  83. 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
  84. 126.
  85. 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
  86. 129.

    Performance Cable Speed Index 0 1250 2500 3750 5000 Original

    FastBoot Optimisations Service Worker 414 972 1,304 2,884