Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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. ?

  2. ?

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

    SESSION Client-side JavaScript Server-side JavaScript
  4. 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)
  5. 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
  6. var CommentBox = React.createClass({ getInitialState: function() { return {data: []};

    }, render: function() { return ( <div className="commentBox"> <h1>Comments</h1> <CommentList data={this.state.data} /> <CommentForm /> </div> ); } });
  7. 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
  8. { "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" }
  9. 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
  10. { "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
  11. (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
  12. <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
  13. 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
  14. importScripts('./localforage.js') onmessage = (e) => { const batch = e.data;

    // Write batch to Indexed DB } …Web Workers? INDEXED DB
  15. 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
  16. // 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
  17. // 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
  18. 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('.') }
  19. 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
  20. 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
  21. Create Project Install Deps Scaffold App Generate Service Worker Build

    step for AppShell Generate App Manifest Run Tests Preprocess CSS Build --mobile
  22. { "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" }
  23. 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!
  24. 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
  25. @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] })
  26. "{ "group": { "app": { "url": { "/app-concat.js": { "hash":

    "2431d95f572a2a23ee6df7d619d2b68ad65f1084" }, "/favicon.ico": { "hash": "164f9754ba7b676197a4974992da8fc3d3606dbf" }, "/icons/android-chrome-144x144.png": { "hash": "2eb2986d6c5050612d99e6a0fb93815942c63b02" }, dist/ngsw-manifest.json
  27. 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
  28. 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
  29. 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
  30. server view displayed server response asset download client init client

    data paint client takes over initial page load
  31. 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
  32. 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
  33. 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;
  34. In my opinion, the data for Angular and Ember flat

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

    Project Svelte Service Worker String Loading
  36. # 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
  37. // 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;
  38. [{ "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" }, ..
  39. { "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" }
  40. //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
  41. 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
  42. Command-line $ ember install ember-cli-fastboot $ ember fastboot —serve-assets #

    Visit your app at http://localhost:3000 $ ember fastboot --environment production
  43. // $ 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
  44. {{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
  45. 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
  46. {{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
  47. Performance Cable Speed Index 0 1250 2500 3750 5000 Original

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