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

PWA, ADVANCED PATTERNS AND STRATEGIES

By Majid Hajian

PWA, ADVANCED PATTERNS AND STRATEGIES

Building progressive web apps has become the norm of web development these days! In fact, we are being introduced to more complex patterns and strategies to create a PWA. For example, When I built my book's website pwawithangular.com, I considered several caching strategies and patterns to deliver fully offline and high-performance web applications that can load under 2 seconds with 3G internet. In this talk, I will share how I could manage to build a fast and reliable PWA by using different caching strategies, advanced PWA architecture, and patterns.

  • 1,466