Share
Embed
Make your Polymer web app Progressive
In this presentation, I describe all PWA features and how to implement them using Polymer
Oleh Zasadnyy
October 22, 2016
Transcript
121 slides
-
Mobile web Apps Monthly unique visitors (MM) 8.9 3.3 Source
(June 2015): https://goo.gl/nArG5B 2.5x -
53% of visits are abandoned if a mobile site takes
more than 3 seconds to load Source (September 2016): https://goo.gl/a3g4J5 1 out of 2 people expect a page to load in less than 2 seconds -
0.0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9
1 Waiting in line at retail store Watching a melodramatic TV show Standing at the edge of a virtual cliff Watching a horror movie Experiencing mobile delays Solving a math problem Source (2015): https://goo.gl/8v7bRG The level of stress caused by mobile delays was comparable to watching a horror movie Cognitive load associated with stressful situations -
77% of mobile sites take longer than 10 seconds to
load on 3G networks Source (September 2016): https://goo.gl/a3g4J5 19 seconds is the average load time for mobile sites on 3G networks -
1.2 1.5 1.8 2.1 2.4 2.7 3.0 3.3 3.6 4.2
4.5 4.8 5.1 5.4 5.7 6.0 6.3 6.6 6.9 7.2 7.5 7.8 8.1 8.4 8.7 9.0 9.3 9.6 9.9 0 20,000 40,000 60,000 80,000 100,000 120,000 140,000 160,000 180,000 3.9 Load time (seconds) Sessions Sessions Conversation rate Source (September 2015): https://goo.gl/qEcjDu 1.9% 1.5% 0.0 0.2 0.4 0.6 0.8 1.0 1.2 1.4 1.6 1.7 Conversion rate (%) 0.98% 1.9x Conversion rate -
Source (September 2015): https://goo.gl/qEcjDu 1.2 1.5 1.8 2.1 2.4 2.7
3.0 3.3 3.6 4.2 4.5 4.8 5.1 5.4 5.7 6.0 6.3 6.6 6.9 7.2 7.5 7.8 8.1 8.4 8.7 9.0 9.3 9.6 9.9 0 20,000 40,000 60,000 80,000 100,000 120,000 140,000 160,000 180,000 3.9 Load time (seconds) Sessions Sessions Bounce rate 56% 12.8% 0 6 13 19 26 32 39 45 51 58 Bounce rate (%) 20% Bounce rate -
Push critical resources for the initial URL route Render initial
route Pre-cache remaining routes Lazy-load and create remaining routes on demand -
<app-drawer-layout fullbleed> <app-drawer> <app-toolbar>Menu</app-toolbar> <iron-selector selected="[[page]]"> <a name="view1" href="/view1">View One</a>
... </iron-selector> </app-drawer> <app-header-layout> ... </app-header-layout> </app-drawer-layout> -
Polymer({ is: ‘my-app', properties: { page: { type: String,
observer: '_pageChanged' } }, _pageChanged: function (page) { var resolvedPageUrl = this.resolveUrl('my-' + page + '.html'); this.importHref(resolvedPageUrl, null, this._showPage404, true); }, _showPage404: function () { this.page = 'view404'; } }); -
Amsterdam, NL - Go Daddy - Chrome - 3G First
View (3.7s) Repeat View (0.6s) -
if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/service-worker.js') .then(function (reg) { console.log('Service
Worker Registered', reg); }) .catch(function (err) { console.log('Error registering Service Worker', err); }); } Register Service Worker -
var cacheName = 'app-shell-cache-v1'; var filesToCache = ['/', '/index.html',...];
self.addEventListener('install', function (event) { event.waitUntil(caches.open(cacheName) .then(function (cache) { return cache.addAll(filesToCache); }).then(function () { return self.skipWaiting(); })); }); Cache Files -
self.addEventListener('activate', function (e) { e.waitUntil( caches.keys().then(function (keyList) { return Promise.all(keyList.map(function
(key) { if (key !== cacheName) { return caches.delete(key); } })); })); return self.clients.claim(); }); Remove Outdated Resources -
self.addEventListener('fetch', function (e) { e.respondWith( caches.match(e.request).then(function (response) { return response
|| fetch(e.request); }) ); }); Return Resources from Cache -
<platinum-sw-register skip-waiting clients-claim auto-register state="{{state}}" on-service-worker-error="handleSWError" on-service-worker-updated="handleSWUpdated" on-service-worker-installed="handleSWInstalled" > <platinum-sw-*>
children which share the service worker registration... </platinum-sw-register> -
{ "entrypoint": "index.html", "shell": "src/my-app.html", "fragments": [ "src/my-view1.html", "src/my-view2.html", "src/my-view3.html",
"src/my-view404.html" ], "sourceGlobs": [ "src/**/*", "images/**/*", "bower.json" ], "includeDependencies": [ "manifest.json", "bower_components/webcomponentsjs/webcomponents-lite.min.js" ] } -
Chrome Desktop and Mobile (version 40+) Firefox Desktop and Mobile
(version 44+) Opera on Mobile (version 27+) -
Browser quota limits Chrome - 6% of free disk space
per origin Firefox - 10% of free disk space (shared across eTLD+1) Safari - at least 10% of free disk space Edge - <8GB: 10MB 8GB-32GB: 50MB 32GB-128GB: 250MB >128GB: 500MB -
<html lang="en"> <head> <meta charset="utf-8"> <title>GDG DevFest Ukraine 2016</title> <link
rel="manifest" href=“manifest.json"> ... -
{ "name": “GDG DevFest Ukraine 2016", "short_name": “GDG DevFest", "icons":
[{ "src": "images/icons/splash-icon-128.png", "sizes": "128x128", "type": "image/png" },{ "src": "images/icons/splash-icon-192.png", "sizes": "192x192", "type": "image/png" },{ "src": "images/icons/splash-icon-384.png", "sizes": "384x384", "type": "image/png" }], "start_url": "/", "display": "standalone", "background_color": "#607d8b", "theme_color": "#607d8b" } -
{ "name": “GDG DevFest Ukraine 2016", "short_name": “GDG DevFest", "icons":
[{ "src": "images/icons/splash-icon-128.png", "sizes": "128x128", "type": "image/png" },{ "src": "images/icons/splash-icon-192.png", "sizes": "192x192", "type": "image/png" },{ "src": "images/icons/splash-icon-384.png", "sizes": "384x384", "type": "image/png" }], "start_url": "/", "display": "standalone", "background_color": "#607d8b", "theme_color": "#607d8b" } -
{ "name": “GDG DevFest Ukraine 2016", "short_name": “GDG DevFest", "icons":
[{ "src": "images/icons/splash-icon-128.png", "sizes": "128x128", "type": "image/png" },{ "src": "images/icons/splash-icon-192.png", "sizes": "192x192", "type": "image/png" },{ "src": "images/icons/splash-icon-384.png", "sizes": "384x384", "type": "image/png" }], "start_url": "/", "display": "standalone", "background_color": "#607d8b", "theme_color": "#607d8b" } -
{ "name": “GDG DevFest Ukraine 2016", "short_name": “GDG DevFest", "icons":
[{ "src": "images/icons/splash-icon-128.png", "sizes": "128x128", "type": "image/png" },{ "src": "images/icons/splash-icon-192.png", "sizes": "192x192", "type": "image/png" },{ "src": "images/icons/splash-icon-384.png", "sizes": "384x384", "type": "image/png" }], "start_url": "/", "display": "standalone", "background_color": "#607d8b", "theme_color": "#607d8b" } -
{ "name": “GDG DevFest Ukraine 2016", "short_name": “GDG DevFest", "icons":
[{ "src": "images/icons/splash-icon-128.png", "sizes": "128x128", "type": "image/png" },{ "src": "images/icons/splash-icon-192.png", "sizes": "192x192", "type": "image/png" },{ "src": "images/icons/splash-icon-384.png", "sizes": "384x384", "type": "image/png" }], "start_url": "/", "display": "standalone", "background_color": "#607d8b", "theme_color": "#607d8b" } -
{ "name": “GDG DevFest Ukraine 2016", "short_name": “GDG DevFest", "icons":
[{ "src": "images/icons/splash-icon-128.png", "sizes": "128x128", "type": "image/png" },{ "src": "images/icons/splash-icon-192.png", "sizes": "192x192", "type": "image/png" },{ "src": "images/icons/splash-icon-384.png", "sizes": "384x384", "type": "image/png" }], "start_url": "/", "display": "standalone", "background_color": "#607d8b", "theme_color": "#607d8b" } -
{ "name": “GDG DevFest Ukraine 2016", "short_name": “GDG DevFest", "icons":
[{ "src": "images/icons/splash-icon-128.png", "sizes": "128x128", "type": "image/png" },{ "src": "images/icons/splash-icon-192.png", "sizes": "192x192", "type": "image/png" },{ "src": "images/icons/splash-icon-384.png", "sizes": "384x384", "type": "image/png" }], "start_url": "/", "display": "browser", "background_color": "#607d8b", "theme_color": "#607d8b" } -
{ "name": “GDG DevFest Ukraine 2016", "short_name": “GDG DevFest", "icons":
[{ "src": "images/icons/splash-icon-128.png", "sizes": "128x128", "type": "image/png" },{ "src": "images/icons/splash-icon-192.png", "sizes": "192x192", "type": "image/png" },{ "src": "images/icons/splash-icon-384.png", "sizes": "384x384", "type": "image/png" }], "start_url": "/", "display": "browser", "background_color": "#607d8b", "theme_color": "#607d8b" } -
{ "name": “GDG DevFest Ukraine 2016", "short_name": “GDG DevFest", "icons":
[{ "src": "images/icons/splash-icon-128.png", "sizes": "128x128", "type": "image/png" },{ "src": "images/icons/splash-icon-192.png", "sizes": "192x192", "type": "image/png" },{ "src": "images/icons/splash-icon-384.png", "sizes": "384x384", "type": "image/png" }], "start_url": "/", "display": "browser", "background_color": "#607d8b", "theme_color": "#607d8b" } -
{ "body": "Did you make a $1,000,000 purchase at Dr.
Evil...", "icon": "images/ccard.png", "vibrate": [200, 100, 200, 100, 200, 100, 400], "tag": "request", "actions": [ { "action": "yes", "title": "Yes", "icon": "images/yes.png" }, { "action": "no", "title": "No", "icon": "images/no.png" } ] } -
Check if User is Subscribed Ask User To Subscribe User
Subscribes Send Subscription Save Subscription Browser Server Subscribing Users -
Sending Messages Generate Message Send to End Point Send to
Browser Received by Browser Server Browser End Point -
if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/service-worker.js') .then(function (reg) { reg.pushManager.getSubscription()
.then(function (sub) { console.log('Subscription Info', sub); }); }); } else { console.log('Subscription Info', 'Not Supported'); } Check for Subscriptions -
var opts = {userVisibleOnly: true, applicationServerKey: pubKey}; navigator.serviceWorker.getRegistration().then(function (reg) {
reg.pushManager.subscribe(opts) .then(function (sub) { console.log('Update Server with sub obj', sub); updateServerWithSubscription(sub); }) .catch(function (error) { console.log('Unable to subscribe user', error); }); }); Subscribe the User -
JSONified Subscription Object { "endpoint": "https://example.com/push/ddj4Sk3abw:A49b...", "keys": { "auth": "qLAYRzG9TnUwbprns6H2Ew==",
"p256dh": "BILXd-c1-zuEQYXH_tc3qmLcqZclw7-Xs0Nlu/sG..." } } -
navigator.serviceWorker.getRegistration().then(function (reg) { reg.pushManager.getSubscription().then(function (sub) { if (sub) { sub.unsubscribe();
console.log('Update our server to remove subscription'); } }); }).catch(function (error) { console.log('Error while trying to unsubscribe', error); }); Unsubscribe the User -
self.addEventListener('push', function (event) { var data; if (event.data) { data
= event.data.json(); } else { // fetch data from server } self.registration.showNotification(data.title, { body: data.body, icon: data.icon, tag: data.tag }); }); Listen For Messages -
self.registration.showNotification(data.title, { body: data.body, icon: data.icons, tag: data.tag, actions: [
{action: 'like', title: 'Like', icon: ic_li}, {action: 'reshare', title: 'Reshare', icon: ic_re} ] }); Add Action Buttons -
Respond To The User self.addEventListener('notificationclick', function (event) { if (event.action
=== 'like') { event.waitUntil(fetch('/like-from-notification')); } else if (event.action === 'reshare') { event.waitUntil(fetch('/reshare-from-notification')); } else { clients.openWindow(event.srcElement.location.origin); } }); -
<platinum-push-messaging id="messaging" title="Application updated" message="The application was updated in the
background" icon-url="icon.png" click-url="notification.html" ></platinum-push-messaging> -
requestPermission: function () { this.$.messaging.enable() .then(function () { // permission
was granted }, function (err) { // permission was denied }); } Subscribe the User -
listeners: { 'platinum-push-messaging-push': 'handleMessage' } handleMessage: function (message) { //
handle received message } Listen for Messages -
requestPermission: function () { this.$.messaging.requestPermission() .then(function () { // permission
was granted }, function (err) { // permission was denied }); } Subscribe the User -
Chrome Desktop and Mobile (version 50+) Firefox Desktop and Mobile
(version 44+) Opera on Mobile (version 37+) -
Stick to transform and opacity changes for your animations Promote
moving elements with will-change or translateZ Avoid overusing promotion rules; layers require memory and management -
transform: translate(npx, npx); transform: scale(n); transform: rotate(neg); transform: skrew(X|Y)(ndeg); transform:
matrix(3d)(...); opacity: 0...1; -
Stick to transform and opacity changes for your animations Promote
moving elements with will-change or translateZ Avoid overusing promotion rules; layers require memory and management -
Stick to transform and opacity changes for your animations Promote
moving elements with will-change or translateZ Avoid overusing promotion rules; layers require memory and management -
Avoid setTimeout or setInterval for visual updates; always use requestAnimationFrame
instead Move long-running JavaScript off the main thread to Web Workers Use micro-tasks to make DOM changes over several frames Use Chrome DevTools’ Timeline and JavaScript Profiler to assess the impact of JavaScript -
Avoid setTimeout or setInterval for visual updates; always use requestAnimationFrame
instead Move long-running JavaScript off the main thread to Web Workers Use micro-tasks to make DOM changes over several frames Use Chrome DevTools’ Timeline and JavaScript Profiler to assess the impact of JavaScript -
var dataSortWorker = new Worker("sort-worker.js"); dataSortWorker.postMesssage(dataToSort); // The main
thread is now free to continue working on other things... dataSortWorker.addEventListener('message', function (evt) { var sortedData = evt.data; // Update data on screen... }); -
Avoid setTimeout or setInterval for visual updates; always use requestAnimationFrame
instead Move long-running JavaScript off the main thread to Web Workers Use micro-tasks to make DOM changes over several frames Use Chrome DevTools’ Timeline and JavaScript Profiler to assess the impact of JavaScript -
var taskList = breakBigTaskIntoMicroTasks(monsterTaskList); requestAnimationFrame(processTaskList); function processTaskList(taskStartTime) { var
taskFinishTime; do { var nextTask = taskList.pop(); // Assume the next task is pushed onto a stack. processTask(nextTask); // Process nextTask. // Go again if there’s enough time to do the next task. taskFinishTime = window.performance.now(); } while (taskFinishTime - taskStartTime < 3); if (taskList.length > 0) requestAnimationFrame(processTaskList); } -
Avoid setTimeout or setInterval for visual updates; always use requestAnimationFrame
instead Move long-running JavaScript off the main thread to Web Workers Use micro-tasks to make DOM changes over several frames Use Chrome DevTools’ Timeline and JavaScript Profiler to assess the impact of JavaScript -
<style is="custom-style"> :root { --layout: { display: flex; };
--layout-inline: { display: inline-flex; }; --layout-horizontal: { @apply(--layout); flex-direction: row; }; ... -
<link rel="import" href="../bower_components/iron-flex-layout/iron-flex-layout.html"> <dom-module id="flex-demo"> <template> <style> :host { @apply(--layout-horizontal);
@apply(--layout-center-center); } .heading { @apply(—layout-flex-2); font-size: 42px; } ... -
3.5 minutes vs 1.1 minutes 40% higher re-engagement rate 70%
greater conversion rate among those arriving via Add to Home Screen 3x lower data usage Live: https://www.flipkart.com -
104% for new users across all browsers 82% increase in
iOS conversion rate 2X more pages visited per session 74% increase in time spent per session across all browsers Live: https://aliexpress.com -
23% increase in mobile search users who return within 7
days 88% improvement in load time for AMP content versus traditional mobile web 1000+ articles The Washington Post publishes in AMP HTML daily Live: https://www.washingtonpost.com