Maxim Salnikov

@webmaxru

Taking your web app offline (in a good sense)

What is an offline-ready web application

And how to build it today

Maxim Salnikov

  • PWAConf igniter [coming soon]

  • PWA Oslo / PWA London meetups organizer

  • ngVikings / Mobile Era conferences organizer

  • Google Dev Expert in Web Tech / Capabilities & Installability

Developer Audience Lead at Microsoft

Web as an app platform is amazing

Browsers

  • Almost on every device with UI

  • Evergreen

  • APIs to access device hardware

Web as an app platform is amazing

JavaScript

Browsers

  • Versatile language

  • Powerful tooling

  • Evolving in a smart way

Web as an app platform is amazing

JavaScript

JS Engines

Browsers

  • Focus on the performance

  • Embedding possibilities

Web as an app platform is amazing

JavaScript

JS Engines

UI Layer

Browsers

  • Convenient tools to build responsive UIs

  • Focus on accessibility

  • Variety of high-quality components and libraries

Web as an app platform is amazing

Community

JavaScript

JS Engines

UI Layer

Browsers

69.7%

Web as an app platform is amazing

Community

JavaScript

JS Engines

UI Layer

Browsers

Issues?

Historically depends on the "connection status"

Solutions

Caching

Installing

  • HTTP Cache?

  • AppCache

  • Save page as... (complete)

  • Chrome Apps

  • Electron

  • NativeScript, React Native

What is PWA at all?

Progressive web apps use modern web APIs along with traditional progressive enhancement strategy to create cross-platform web applications.

These apps work everywhere and provide several features that give them the same user experience advantages as native apps.

works everywhere*

* but not everything**

natively

** use progressive enhancement strategy

Offline-

ready  

<

>

=

Proper offline-ready web app

  • App itself

  • Online runtime data

  • Offline runtime data

  • Connection failures

  • Updates

  • Platform features

  • Always available

  • Thoughtfully collected

  • Safely preserved

  • Do not break the flow

  • Both explicit and implicit

  • For the win!

While keeping its web nature!

Application UI

Let's build an App shell

My App

  • Define assets

  • Put in the cache

  • Serve from the cache

  • Manage versions

}

Service worker

Logically

Physically

-file(s)

App

Service worker

Browser/OS

Event-driven worker

Cache

fetch
push
sync

Own service worker

self.addEventListener('install', event => {
    // Use Cache API to cache html/js/css
})

self.addEventListener('activate', event => {
    // Clean the cache from the obsolete versions
})

self.addEventListener('fetch', event => {
    // Serve assets from cache or network
})

handmade-service-worker.js

It's only partially a joke

Because...

Redirects?

Fallbacks?

Opaque response?

Versioning?

Cache invalidation?

Spec updates?

Cache storage space?

Variable asset names?

Feature detection?

Minimal required cache update?

Caching strategies?

Routing?

Fine-grained settings?

Kill switch?

I see the old version!!!

  • Define assets

  • Put in the cache

  • Serve from the cache

  • Manage versions

}

Is there a helper?

While having our own service worker

  • Application shell

  • Runtime caching

  • Replaying failed network requests

  • Offline Google Analytics

  • Broadcasting updates

# Installing the Workbox Node module
$ npm install workbox-build --save-dev

Configuration

// Sample configuration with the basic options
var workboxConfig = {
  globDirectory: 'dist/',
  globPatterns: [
    '**/*.{txt,png,ico,html,js,json,css}'
  ],
  swSrc: 'src/workbox-service-worker.js',
  swDest: 'dist/sw.js'
}

workbox-build.js

Source service worker

import { precacheAndRoute } from "workbox-precaching";

// Precaching and setting up the routing
precacheAndRoute(self.__WB_MANIFEST)

src/workbox-service-worker.js

Caching, serving, managing versions

Build service worker

// We will use injectManifest mode
const {injectManifest} = require('workbox-build')

// Sample configuration with the basic options
var workboxConfig = {...}

// Calling the method and output the result
injectManifest(workboxConfig).then(({count, size}) => {
    console.log(`Generated ${workboxConfig.swDest},
    which will precache ${count} files, ${size} bytes.`)
})

workbox-build.js

Build flow integration

{
  "scripts": {
    "build-sw": "node workbox-build.js && npx rollup -c",
    "build":    "npm run build-app && npm run build-sw"
  }
}

package.json

Runtime application data

Intercepting requests

self.addEventListener('fetch', event => {

  if (event.request.url.indexOf('/api/breakingnews') != -1) {
    event.respondWith(
      // Network-First Strategy
    )
  } else if (event.request.url.indexOf('/api/archive') != -1 {
    event.respondWith(
      // Cache-First Strategy
    )
  }
})

handmade-service-worker.js

Routes and strategies

import { registerRoute } from "workbox-routing";
import * as strategies from "workbox-strategies";

registerRoute(
  new RegExp('/api/breakingnews'),
  new strategies.CacheFirst()
);

src/workbox-service-worker.js

registerRoute(
  new RegExp('/api/archive'),
  new strategies.CacheFirst({plugins: [...]})
);

Strategies

  • CacheFirst

  • CacheOnly

  • NetworkFirst

  • NetworkOnly

  • StaleWhileRevalidate

Plugins

  • Expiration

  • CacheableResponse

  • BroadcastUpdate

  • BackgroundSync

  • ...your own plugin?

Save and sync offline actions

Background sync

  • One-off event for offline -> online

  • No active app tab required

navigator.serviceWorker.ready.then( swRegistration => {
  return swRegistration.sync.register('postTweet');
});

main.js

self.addEventListener('sync', event => {
  if (event.tag == 'postTweet') {
    event.waitUntil(
        // Do useful things...
    );
  }
});

handmade-service-worker.js

import {BackgroundSyncPlugin} from 'workbox-background-sync';

const postTweetPlugin =
    new BackgroundSyncPlugin('tweetsQueue', {
        maxRetentionTime: 24 * 60 // Max retry period
    })

src/workbox-service-worker.js

registerRoute(
  /(http[s]?:\/\/)?([^\/\s]+\/)post-tweet/,
  new strategies.NetworkOnly({plugins: [postTweetPlugin]}),
  'POST'
)

Schedule your updates

Periodic background sync

  • App installed

  • Online

  • Original network

  • Occasional interactions

  • No cadence control

  • Shortest break: 12 hours

Periodically run custom code without notifying user

const registration = await navigator.serviceWorker.ready;
if ('periodicSync' in registration) {
  try {
    registration.periodicSync.register('refreshTweets', {
      // An interval of one day.
      minInterval: 24 * 60 * 60 * 1000,
    });
  } catch (error) {
    // PBS cannot be used.
  }
}

main.js

self.addEventListener('periodicsync', (event) => {
  if (event.tag === 'refreshTweets') { 
    event.waitUntil(
    	fetchNewTweets() // Maybe
    );
  }
});

src/service-worker.js

  • Network connection type?

  • Data saver mode?

  • Available storage space?

Debugging background services

Chromium -> DevTools -> Application

PWA Background Services

Online, tonight 19:00CEST

bit.ly/pwa-bg

More platform tools

Native File System API

Badging API

Contact Picker API

Features flying soon

More than 100 new APIs

  • Full-fledged application platform

  • Offline-ready mechanisms are in production

  • Awesome tools are available

  • User experience & security is the key

And this is just the beginning!

Web platform today

  • 2000+ developers

  • Major browsers/frameworks/libs reps

Thank you!

Maxim Salnikov

@webmaxru

Questions?

Maxim Salnikov

@webmaxru

Made with Slides.com