Slide 1

Slide 1 text

Progressive Web Apps Across All Frameworks + AddyOsmani @addyosmani

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

Built with a JavaScript framework / library Existing apps Progressively Enhanced

Slide 4

Slide 4 text

Web App install banner Splash screen Offline support

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

?

Slide 7

Slide 7 text

Web App install banner Splash screen Offline support

Slide 8

Slide 8 text

Add to home screen Offline support Web App install banner

Slide 9

Slide 9 text

Mobile Safari on iOS

Slide 10

Slide 10 text

?

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

No content

Slide 14

Slide 14 text

No content

Slide 15

Slide 15 text

No content

Slide 16

Slide 16 text

No content

Slide 17

Slide 17 text

No content

Slide 18

Slide 18 text

INSTANT

Slide 19

Slide 19 text

Time to first paint

Slide 20

Slide 20 text

Time to first meaningful paint

Slide 21

Slide 21 text

Time to first meaningful interaction

Slide 22

Slide 22 text

No content

Slide 23

Slide 23 text

No content

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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)

Slide 26

Slide 26 text

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 { } loadCSS

Slide 27

Slide 27 text

No content

Slide 28

Slide 28 text

No content

Slide 29

Slide 29 text

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

Comments

); } });

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

No content

Slide 32

Slide 32 text

2014

Slide 33

Slide 33 text

No content

Slide 34

Slide 34 text

No content

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

No content

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

React HN

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

No content

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

(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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

No content

Slide 47

Slide 47 text

Go offline. App shell cached! Current session data.

Slide 48

Slide 48 text

BUT…relaunch and…

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

No content

Slide 51

Slide 51 text

INDEXED DB LOCALFORAGE DEXIE IDB

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

Text

Slide 54

Slide 54 text

After loading just one page of comments.

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

No content

Slide 59

Slide 59 text

No content

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

No content

Slide 62

Slide 62 text

No content

Slide 63

Slide 63 text

No content

Slide 64

Slide 64 text

// 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

Slide 65

Slide 65 text

// 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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

No content

Slide 69

Slide 69 text

Angular 1.5?

Slide 70

Slide 70 text

Add to home screen Splash screen Offline support

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

Angular Mobile Toolkit

Slide 73

Slide 73 text

Angular CLI

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

App Manifest

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

Application Shell

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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!

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

@Component({ moduleId: module.id, selector: 'hello-mobile-app', template: ` {{title}}
App is Fully Rendered
`, directives: [APP_SHELL_DIRECTIVES, MdToolbar, MdSpinner] })

Slide 83

Slide 83 text

shellRender shellNoRender

Slide 84

Slide 84 text

SERVICE WORKER

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

OFFLINE COMPILATION

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

Angular Universal

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

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

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

No content

Slide 97

Slide 97 text

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;

Slide 98

Slide 98 text

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

Slide 99

Slide 99 text

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

Slide 100

Slide 100 text

No content

Slide 101

Slide 101 text

No content

Slide 102

Slide 102 text

# 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

Slide 103

Slide 103 text

// 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;

Slide 104

Slide 104 text

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

Slide 105

Slide 105 text

No content

Slide 106

Slide 106 text

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

Slide 107

Slide 107 text

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

Slide 108

Slide 108 text

No content

Slide 109

Slide 109 text

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

Slide 110

Slide 110 text

No content

Slide 111

Slide 111 text

Command-line $ ember install broccoli-serviceworker

Slide 112

Slide 112 text

//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

Slide 113

Slide 113 text

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

Slide 114

Slide 114 text

No content

Slide 115

Slide 115 text

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

Slide 116

Slide 116 text

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

Slide 117

Slide 117 text

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

Slide 118

Slide 118 text

// $ 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

Slide 119

Slide 119 text

No content

Slide 120

Slide 120 text

No content

Slide 121

Slide 121 text

Works without JavaScript too.

Slide 122

Slide 122 text

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

Slide 123

Slide 123 text

{{content-for "head"}} {{content-for 'critical-path-css'}} {{content-for "head-footer"}} {{content-for "body"}} {{content-for "body-footer"}} index.html App Shell CSS

Slide 124

Slide 124 text

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

Slide 125

Slide 125 text

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

Slide 126

Slide 126 text

No content

Slide 127

Slide 127 text

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

Slide 128

Slide 128 text

{{content-for "head"}} {{content-for 'critical-path-css'}} {{content-for "head-footer"}} {{content-for "body"}} {{content-for "body-footer"}} index.html Deferred script execution

Slide 129

Slide 129 text

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

Slide 130

Slide 130 text

DO LESS lazy BE

Slide 131

Slide 131 text

DESIGN FOR ENVIRONMENTS constrained

Slide 132

Slide 132 text

GO PROGRESSIVE g.co/ProgressiveWebApps

Slide 133

Slide 133 text

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