Advanced Patterns And Strategies

Majid Hajian

mhadaily

Reliable

Fast

Engaging

mhadaily

mhadaily

Agenda

  • Background Sync
  • Adaptive Loading
  • Advanced Caching
  • Stream

ME.


MaterialApp(
   ThemeData(
        name: "Majid Hajian",
        location: "Oslo, Norway",
        description: '''
        	Passionate Software engineer, 
	        Community Leader, Author and international Speaker
         ''',
        main: "Flutter/Dart, PWA, Performance",
        homepage: "https://www.majidhajian.com",
        socials: {
          twitter: "https://www.twitter.com/mhadaily",
          github: "https://www.github.com/mhadaily"
        },
        author: {
          Pluralsight: "www.pluralsight.com/authors/majid-hajian",
          Apress: "Progressive Web App with Angular, Book",
          PacktPub: "PWA development, 7 hours video course",
          Udemy: "PWA development, 7 hours video course",
        }
        founder: "Softiware As (www.Softiware.com)"
        devDependencies: {
          tea: "Ginger", 
          mac: "10.14+",
        },
        community: {
          MobileEraConference: "Orginizer",
          FlutterVikings: "Orginizer", 
          FlutterDartOslo: "Orginizer",
          GDGOslo: "Co-Orginizer",
          DevFestNorway: "Orginizer",
          ...more
        })
      );

mhadaily

Find me on the internet by

Cache API

XHR vs. Fetch API

let xhr = new XMLHttpRequest();
xhr.open('GET', 'http://domain/service');

// request state change event
xhr.onreadystatechange = function() {

  // request completed?
  if (xhr.readyState !== 4) return;

  if (xhr.status === 200) {
    // request successful - show response
    console.log(xhr.responseText);
  }
  else {
    // request error
  }
};

// start request
xhr.send();
fetch(
    'http://domain/service',
    { method: 'GET' }
  )
  .then( response => response.json() )
  .then( json => console.log(json) )
  .catch( error => console.error('error:', error) );

mhadaily

Cache API

Service Worker

mhadaily

self.addEventListener('fetch', (event) => {
  console.log('[SW] Fetch ....');
  const request = event.request;
  event.respondWith()
}

FETCH EVENT

Service worker

mhadaily

event.respondWith(
	caches.match(request)
);

Cache only

Service worker

mhadaily

event.respondWith(
	fetch(event.request)
);

Network only

Service worker

mhadaily

event.respondWith(
  caches.match(request).then( (res) => {
    return res || fetch(request).then( (newRes) => {
        caches.open(DYNAMIC_CACHE_VERSION)
        .then( cache => cache.put(request, newRes) );
      return newRes.clone();
    });
  })
);

Cache first, falling back to the network

Service worker

mhadaily

event.respondWith(
  fetch(request)
    .then((res) => {
      caches.open(DYNAMIC_CACHE_VERSION)
  	    .then(cache => cache.put(request, res));
      return res.clone();
    }) // Fallback to cache
  .catch(err => caches.match(request))
);

The network first, falling back to the cache

Service worker

mhadaily

event.respondWith(
caches
    .match(request).then((res) => {
      const updatedResopnse = fetch(request)
      	.then((newRes) => {
    	  cache.put(request, newRes.clone());
      return newRes;
    });
   return res || updatedResopnse;
  })
);

Cache with Network Update

Service worker

mhadaily

const promiseRace = new Promise((resolve, reject) => {
  let firstRejectionReceived = false;
    const rejectOnce = () => {
      if (firstRejectionReceived) {
        reject('No response received.');
      } else {
        firstRejectionReceived = true;
      }
    };
    fetch(request)
        .then(res => res.ok ? resolve(res) : rejectOnce())
        .catch(rejectOnce);
      caches.match(request)
        .then(res => res ? resolve(res) : rejectOnce())
        .catch(rejectOnce);
    });
event.respondWith(promiseRace);

Cache & Network Race

Service worker

mhadaily

mhadaily

Background Sync

API

mhadaily

mhadaily

workbox.routing.registerRoute(
  new RegExp('^https:\/\/my-api-server/post\/'),
  workbox.strategies.networkOnly({
    plugins: [bgSyncPlugin]
  }),
  'POST'
)

mhadaily

const bgSyncPlugin = new workbox.backgroundSync.Plugin('myQueueName', {
  maxRetentionTime: 24 * 60 // Retry for max of 24 Hours
  onSync: async((queue)=>{
    let entry;
  
  	while ((entery = await queue.shiftRequest())){
      
         try {
           // Perform fetch request and perhaps show notificaiton to user 
           // you can also update cache 

         } catch(error) {
           // remove request from the queue
         
         }
      	
    }
  
  });
});

mhadaily

const showNotification = () => {
  self.registration.showNotification('Post Sent', {
    body: 'You are back online and your post was successfully sent!',
    icon: 'assets/icon/256.png',
    badge: 'assets/icon/32png.png'
  });
};

mhadaily

Loading based on connectivity speed

adaptiveLoading

mhadaily

navigator.connection.effectiveType

2G

3G

4G

mhadaily

2G

3G

4G

mhadaily

workbox.routing.registerRoute(
  new RegExp('/img/'),
  workbox.strategies.cacheFirst({
    cacheName: 'images',
    plugins: [
      adaptiveLoadingPlugin,
      workbox.expiration.Plugin({
        maxEntries: 50,
        purgeOnQuotaError: true,
      }),
    ],
  }),
);

mhadaily

const adaptiveLoadingPlugin = {
  requestWillFetch: async ({request}) => {
    const urlParts = request.url.split('/');
    let imageQuality;

    switch (
      navigator && navigator.connection
        ? navigator.connection.effectiveType
        : ''
    ) {
      //...
      case '3g':
        imageQuality = 'low_quality';
        break;
      //...
    }

    const newUrl = urlParts
      .splice(urlParts.length - 1, 0, imageQuality)
      .join('/')
      .replace('.jpg', '.png');
    
    const newRequest = new Request(newUrl.href, {headers: request.headers});

    return newRequest;
  },
};

mhadaily

Advanced

App Shell

with Stream

PRECACHE

mhadaily

PRECACHE

shell_header.html

shell_footer.html

mhadaily

Dynamically changes

mhadaily

Streaming

Fetch

Process

Fetch

Render

mhadaily

registerRoute(/\.html$/,
  workbox.streams.strategy([

    // Get from cache 
    () => cacheFirst.handle({
        request: new Request("/shell_header.html"),
    }),

    //  Get the body from the network
    ({request}) => networkFirst.handle({
      request: `${request.url}?content=true`
    });

    // Get from cache 
    () => cacheFirst.handle({
        request: new Request("/shell_footer.html"),
    }),

  ]);
);

mhadaily

Summary

mhadaily

Agenda

  • Background Sync
  • Adaptive Loading
  • Advanced Caching
  • Stream

The web platform today is way more powerful than what we think!

 

let's embrace it!

Majid Hajian

mhadaily

Slides and link to source code

bit.ly/pwa-patterns

majid[at]softiware[dot]com

SVG icons credited to undraw.co