PWA: Service Worker Secrets, Tricks, Debugging

Quick summary ↬ You will find answers to questions about problems with the service worker that you have been asking for a long time, but could not find answers anywhere on the Internet.
PWA Service Worker How To
Posted

If you are new to Progressive Web Apps, I highly recommend you read my  Progressive Web Apps (PWA) Explained Simply article. Here I cover topics for advanced users. I ran into all of these issues when I was developing PWAs and found almost no information about it online. I read documentation, overcome problems and share this information with you. At the end of this article, I share my “Website Booster & Offline Denis.es Blog Service Worker”.

1. Warning: “Does not register a service worker that controls page and start_url

So you've placed the service worker file to the root (or not), or set the scope to "/" in the manifest file, but something is still wrong?

Lighthouse reports:

This page is controlled by a service worker, however the `start_url` (https://www.example.com/) is not in the service worker's scope (https://www.example.com/oh-i-coded-the-bug/faq/)

And the link leads here.

Or the Application section of DevTools says:

No matching service worker detected. You may need to reload the page, or check that the scope of the service worker for the current page encloses the scope and start URL from the manifest.

You can read a lot of posts on the internet, but what you really need to know is the following if you really need your application to cover all the pages of your website:

Register service worker for / not ./

However, this assumes that your service worker file is placed to the root (usually private_html). If it doesn't and you don't like the idea of having this file in the root because you want it to be versioned with your assets and you don't have control over your webserver, then the easiest way is to create a symlink to your service worker file in assets from private_html and then you register the service worker file by pointing to that location in the root (i.e. to that symlink) while you still keep your real service worker file in assets.

However, if you control your webserver and don't like the symlink idea, you should add the following to your nginx configuration:

add_header Service-Worker-Allowed /;

In the blog you are reading, you can see that the service worker file is in /assets/andy/pwa/service-worker-denises.js and you can see that the registration is for the / scope. It's because I edited the nginx config.

If you want to understand the service worker life cycle (and you should!), you should read this article.

2. How can I debug a service worker with OpenServer on a Windows PC?

PWA debugging on localhost requires an SSL connection. All other old approaches with enabling certain settings in Chrome etc. will no longer work. So the question is actually about how to set up https for your local environment, especially when it's not a localhost, but a set of different domains.

Main starting point: https://web.dev/how-to-use-local-https/

If you're on Windows, go to https://github.com/FiloSottile/mkcert or directly to https://github.com/FiloSottile/mkcert/releases.

Download the .exe file, rename it to mkcert.exe and put it in, say, D:\OpenServer\userdata\config\cert_files

Run PowerShell as an administrator and run the following:

cd D:\OpenServer\userdata\config\cert_files
./mkcert -install
./mkcert www.domain1.local www.domain2.local www.domain3.local

You must list all your domains from D:\OpenServer\domains if you want to use https:// for all of them.

You will get one certificate for all of them in D:\OpenServer\userdata\config\cert_files which will be named www.domain1.local+2.pem where www.domain1.local is your first domain from the list and 2 is the number of additional domains.

Then you have to edit D:\OpenServer\userdata\config\Apache_2.4-PHP_7.2-7.3-x64+Nginx_1.17_vhostn.conf and change the default values there to the following:

ssl_certificate               '%sprogdir%/userdata/config/cert_files/www.domain1.local+2.pem.pem';
ssl_certificate_key           '%sprogdir%/userdata/config/cert_files/www.domain1.local+2.pem-key.pem';

When the certificates are out of date, you simply run the same command again:

./mkcert www.domain1.local www.domain2.local www.domain3.local

Remember that after you change the list of domains in the command, the file name will change and you will need to edit the config again! Of course, you can rename them as you like.

Then you should edit D:\OpenServer\userdata\config\Apache_2.4-PHP_7.2-7.3-x64+Nginx_1.17_vhostn.conf with the following:

add_header Service-Worker-Allowed /;

This helps overcome / scope registration issue.

3. How to solve the problem of a renamed service worker?

Listen a wise man: NEVER ever rename your service worker.

So, you have renamed your service worker, and now the old one will not be unregistered on your users' devices. Of course it won't because you no longer control it - the browser controls it. I once maintained a dozen sites and had a common service worker at the root for them. Later, I decided to make separate service workers for them and put the service worker files in the assets/ folder, and not in the root (see issue #1 above). I ran into the problem that without manually unregistering the service worker in DevTools, I can't get the new service worker to take control of the page, which makes sense. Tried googling "how to uninstall old service worker", "I renamed service worker, how to uninstall", "how to deal with renamed service worker", "unregister old service worker", etc... couldn't find any solution. Well the documentation you have, but the key point is "old" and "renamed". Since there was no ready-made solution, I came up with my own and share it with you. Instead of the "regular" (or basic) service worker registration script that you can find in my beginner's guide, I now have to use the following script:

<script>
// Service Worker Registration (Credits to Denis.es Blog: https://www.denis.es/blog/pwa-service-worker-problems-secrets-tricks-debugging/)
const SW_FILE = '/assets/andy/pwa/service-worker-denises.js';
if ('serviceWorker' in navigator) {
    // Handling service worker rename cases
    if (navigator.serviceWorker.controller && navigator.serviceWorker.controller.scriptURL.includes(SW_FILE)) {
        console.log('Active service worker found, no need to register')
    } else {
        // Unregister all possible old named service workers
        navigator.serviceWorker.getRegistrations().then(function(registrations) {
            for (let registration of registrations) {
                registration.unregister();
                console.log('Unregister old service worker(s)')
            }
        });
        // Register the service worker
        navigator.serviceWorker.register(SW_FILE, {
            scope: '/'
        }).then(function(reg) {
            console.log('Service worker',SW_FILE,'has been registered for scope: '+ reg.scope);
        }).catch(function(err) {
            console.error(SW_FILE, 'Service worker',SW_FILE,'registration failed', err);
        });
    }
}
</script>

Take my advice: never rename your service worker and you won't need to keep this overly unnecessary piece of code on your page.

I post useful information on my blog and share my experience for free. I don't post links to Amazon or anything like that. If my articles educated you, or maybe even helped you earn or save money or find a job, please consider donating to support me writing and sharing more knowledge. How will I use donations? I will buy food for cats that we have adopted from shelters.

4. How can I debug a service worker directly on my android phone?

Ah, that was something... I decided to debug PWA directly on the phone - for some reason the service worker didn't work on Android. Well, no way... What's the problem? First of all, I couldn't connect my phone to my Windows PC. The computer sees my phone's storage, but nothing more. From my previous experience, I remember that my phone was supposed to have a USB debugging option, but I couldn't find it anymore. Many online resources also mention the "More Tools -> Remote Devices" option in DevTools, but it's not there! Okay, you'll find everything here.

Enable the USB debugging option on your Android phone:

ГЫИ вуигппштп menu on Android

If you don't see it, here's the trick. Click 7 times on the build number:

Developer options menu on Android - how to access

And you will access the Developer options menu where you can find USB debugging:

Developer options menu on Android

Go to chrome://inspect/#devices and you will see your phone there: 

Debug Progressive Web App remotely on your Android phone from Chrome DevTools

Here is an app with an install prompt on Android:

Installation prompt for the app on Android phone

And here is how you see it on your computer:

Debug service worker remotely directly on your Android phone

Here's a live demo of how debugging works. As you can see, you can even observe screen rotation on your computer. Sorry for the shaky video. A phone with a good camera is in the frame. ;)

5. How to restore the offline assets we cached during the service worker install event?

I was wondering why no one ever bothered about this when I read the documentation from Google, Microsoft and MDN. Look at the problem. We usually add the resources needed for the offline page during install event, as recommended in most of the articles you've probably read. However, if the service worker is already installed, these resources will not be added if they were removed from the cache for some reason. Why aren't they added anymore? Because the service worker will not be re-installed if it is already registered and controlling the page. Why is the cache cleared but the service worker remains registered? Well, Apple with their Safari will do it for sure. The user can clear the cache and we can't be sure that the service worker will be cleared at the same time (hopefully it will). So, I figured out how to restore these assets without the install event.

Briefly, the concept is as follows. You must have some kind of resource that will only be cached during installation. Then you have to verify it's still in the cache. Verify when? When your service worker handles other assets you have on each page.  You will find how I implemented this concept in my "Website Booster & Offline Denis.es Blog Service Worker" below. 

6. How to make Google Fonts faster and avoid weird fonts glitches on page refresh?

So, are you concerned about flashing unstyled text (FOUT) on page load? Yes, it's very annoying, especially on a slow connection. This also results in a layout shift, which affects Cumulative Layout Shift (CLS) metrics. Google has announced that starting in Chtome 83, with rel="preload" and font-display: optional you can completely remove layout jank. There are plenty of links to this topic. One of the most detailed is from Harry. However, I found it easier to read this one from Brandon. Adrian Bece sums up the FOUT topic nicely.

However, none of them seem to address the topic of caching Google Fonts with a service worker! Try F5 a few times on this page - do you see glitches with fonts? You should not. Close the browser and open the page again, or disable caching (checkbox in the "Network" section in DevTools). No fonts glitches, right?

Of course, you can avoid flickering fonts by loading them in a resource-blocking way, but we won't do that or we'll run into a serious performance problem. However, once you try "lazy loading" fonts, you tend to run into this font jumping problem, especially with the rather dubious &display=swap option, which is recommended to be placed at the end of the URL, because that way you actually deliberately telling the browser to use system fonts first. Without the &display=swap option, you won't have font glitches even if the browser cache is disabled, as long as you cache fonts to local storage using a service worker. To be honest, I don't know if anyone is surfing with the browser cache disabled, but the browser cache (the one you can disable in the Network section of the DevTools) only persists until you close the browser, so on return visit the fonts will be downloaded again. This is not the case if they are in local storage and only a service worker can put them there.

If you don't use the &display=swap option, Lighthouse will complain about being able to check text visibility when loading web fonts. In any case, I decided to skip this option, and yes, Lighthouse will certainly complain, but usually shows a 0ms performance drop.

Let's summarize. Apply all the advice from Garry's post except the &display=swap option (or it's up to you) and use a service worker to cache fonts in local storage. See my "Website Booster & Offline Denis.es Blog Service Worker" below.

7. Why assets on my offline page are not available even if I cached them?

Ha… I bet you missed the point that your offline page is also controlled by the service worker! I have found that many people miss this point. Imagine that you have cached resources for the offline page and only use the Network-first strategy with a fallback to the offline page. How will your page's resources be served if you fetch them online when you're offline? You should have a default 'cache-first' or 'cache-only' strategy for the offline scenario. The reality is more complex - see my well-commented "Website Booster & Offline Denis.es Blog Service Worker" below for details.


Website Booster & Offline Denis.es Blog Service Worker 2022

I covered the main aspects of PWA in my Progressive Web Apps (PWA) Explained Simply article, which is mainly aimed for beginners. As I mentioned, offline mode and caching are some of the most common reasons why you need a service worker. I created a service worker for this blog to address these two things. You can open my service worker file - it's fully documented. You should find a lot of useful information there. I cleared it up a bit and provided a copy below.

I post useful information on my blog and share my experience for free. I don't post links to Amazon or anything like that. If my articles educated you, or maybe even helped you earn or save money or find a job, please consider donating to support me writing and sharing more knowledge. How will I use donations? I will buy food for cats that we have adopted from shelters.

Features of my service worker:

  • Addresses an issue where Google Fonts glitches on page reload;
  • Uses multiple caching strategies;
  • Speed up your site by over 90%;
  • Offline page fallback when user is offline or server not responding;
  • Ability to recover offline assets;
  • Allow website developers to edit JS/CSS almost independently of the service worker;
  • Easy to understand syntax (I hope).

/*  Website Booster & Offline Denis.es Blog Service Worker
    https://www.denis.es/blog/pwa-service-worker-problems-secrets-tricks-debugging/
*/


// Increment this each time you change assets that are not part of the 'Stale-while-revalidate' strategy.
const VERSION = 'v1_0_2'; 

const CACHE_NAME = `denises-${VERSION}`;

// Offline page with manual and automatic reload based on the online event and regular server polling.
const OFFLINE_URL = '/assets/andy/pwa/offline-denises-advanced.html';

// A special asset for detecting the deletion of our offline assets. It's actually an image that you either only use on an offline page or do not use at all.
const OFFLINE_ASSETS_LOST_IMG = '/assets/andy/img/offline.png';
// The problem: We add the resources required for the offline page during 'install' event. 
// However, if the service worker is already installed, these resources will not be added if they were removed 
// from the cache for some reason (Apple with their Safari will do this for sure). At the same time, we don't 
// have these assets on every page because we use minified JS and CSS on other pages. This means that we must 
// regularly update the offline page assets on one basis or another. To do this, I came up with this "hack".
// Why not use the page itself? I have found that some browsers keep HTML longer than other assets.

// Static resources to cache initially (or as Google call it "Cache the application shell" https://developers.google.com/web/ilt/pwa/lab-caching-files-with-service-worker)
// Setting {cache: 'reload'} in the new request will ensure that the response isn't fulfilled from the HTTP cache; i.e., it will be from the network.
const INITIAL_CACHED_RESOURCES = [
  //new Request('/', { cache: "reload" }), // This will put the root page, actually the HTML of that page (or destination=document), and we don't need that because we don't want the HTML to be served from the cache other than the offline page. 
  new Request(OFFLINE_URL, { cache: "reload" }), // HTML of our offline page. 
  new Request(OFFLINE_ASSETS_LOST_IMG, { cache: "reload" }), 
  //new Request('/assets/andy/pwa/service-worker-denises.js', { cache: "reload" }), // The reason for caching the service worker has not yet been found.
  new Request('/assets/andy/pwa/manifest-denises.json?v1=2022051601', { cache: "reload" }), // Since we are calling the manifest on every page, let's cache it.
  
  // Below we should list all the assets of our offline page and anything else
  // you want to cache on install, which might come in handy for browsing other pages soon.
  
  new Request('https://fonts.googleapis.com/css?family=Montserrat:400,700', { cache: "reload" }),
  new Request('https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800', { cache: "reload" }),

  new Request('/assets/common/img/ui.totop.png', { cache: "reload" }),
  new Request('/media/img-uncuted-common/den.jpg', { cache: "reload" }),
  
  new Request('/assets/andy/css/bootstrap.min.css', { cache: "reload" }),
  new Request('/assets/common/css/font-awesome-our.min.css', { cache: "reload" }),
  new Request('/assets/andy/css/style.min.css', { cache: "reload" }),
  new Request('/assets/andy/iot-den/styles.min.css', { cache: "reload" }),
  new Request('/assets/andy/css/colors/green.min.css', { cache: "reload" }),
  
  new Request('/assets/andy/js/blog-essentials-v1.min.js', { cache: "reload" }),
  new Request('/assets/andy/plugins/highlight/highlight-our.min.js', { cache: "reload" }),
  new Request('/assets/andy/js/jquery-migrate-3.4.0.min.js', { cache: "reload" }),
  new Request('/assets/andy/js/inview-protonet.min.js', { cache: "reload" }),
  new Request('/assets/andy/js/jquery.stellar.min.js', { cache: "reload" }),
  new Request('/assets/common/plugins/jquery-validate/jquery.validate.min.js', { cache: "reload" }),
  new Request('/assets/andy/js/jquery-easing-1.3.min.js', { cache: "reload" }),
  new Request('/assets/andy/js/jquery.ui.totop.min.js', { cache: "reload" }),
  new Request('/assets/common/plugins/jssocials/jssocials-0.2.0-our_mod.min.js', { cache: "reload" }),
  new Request('/assets/common/plugins/jquery.countdown/jquery.countdown.min.js', { cache: "reload" }),
  new Request('/assets/common/plugins/d3/d3.min.js', { cache: "reload" }),
  new Request('/assets/andy/js/d3TextFlow.min.js', { cache: "reload" }),
  new Request('/assets/andy/js/script_v2.min.js', { cache: "reload" }), // We may change this JS often.
 
  // From the manifest file and meta.
  new Request('/assets/andy/icons/denis/favicon-sketch-48x48.png?v1=2022051601', { cache: "reload" }),
  new Request('/assets/andy/icons/denis/apple-touch-icon-sketch-192x192.png?v1=2022051601', { cache: "reload" }),
  new Request('/assets/andy/icons/denis/round-sketch-192x192.png?v1=2022051601', { cache: "reload" }),
  new Request('/assets/andy/icons/denis/round-sketch-512x512.png?v1=2022051601', { cache: "reload" }),
  new Request('/assets/andy/icons/denis/maskable-sketch-192x192.png?v1=2022051601', { cache: "reload" }),
  new Request('/assets/andy/icons/denis/maskable-sketch-512x512.png?v1=2022051601', { cache: "reload" }),
];

self.addEventListener('install', (event) => {
// Use the install event to pre-cache all initial resources.
// If any assets fail to cache, the service worker installation is aborted.
  console.log('[Service Worker] Install');
  event.waitUntil((async () => {
      const cache = await caches.open(CACHE_NAME);
      console.log('[Service Worker] Caching INITIAL_CACHED_RESOURCES on install');
      await cache.addAll(INITIAL_CACHED_RESOURCES);
  })());
  // Force the waiting service worker to become the active service worker:
  self.skipWaiting();
});

self.addEventListener('activate', (event) => {
// Use the activate event to enable navigation preload and to delete old caches and avoid running out of space.
  event.waitUntil((async () => {
    // Enable navigation preload if it's supported by browser.
    // MUST read and understand: https://developers.google.com/web/updates/2017/02/navigation-preload
    if ('navigationPreload' in self.registration) {
      await self.registration.navigationPreload.enable();
      console.log('[Service Worker] navigationPreload is supported');
    }
    // Delete old cache.
    const names = await caches.keys();
    await Promise.all(names.map(name => {
      if (name !== CACHE_NAME) {
        return caches.delete(name);
      }
    }));
  })());
  // Tell the active service worker to take control of the page immediately:
  self.clients.claim();
});

// ###########################################################################
// ####### 'Network-only' strategy for HTML with offline page fallback #######
// ###########################################################################

// Almost a direct copy of https://web.dev/offline-fallback-page/ with my detailed comments.
// This strategy means that we will never display HTML (or 'document') from the cache, even if it was somehow placed in the cache.
// If we tried the 'Cache-first' strategy here, we would never fall back to our offline page if the HTML ('document') is in the cache.
// And then we would also need a mechanism to keep this cache up to date.
self.addEventListener('fetch', (event) => {

    // We will only call event.respondWith() if this is a navigation request for an HTML page.
    // See https://developer.mozilla.org/en-US/docs/Web/API/Request/mode
    // This allows us to treat page assets differently in the handlers that follow this one.
    if (event.request.mode === 'navigate') {
    event.respondWith((async () => {
    try {            
          // First, try to use the navigation preload response if it's supported (must, if you enabled it!):
          const preloadResponse = await event.preloadResponse;
          if (preloadResponse) return preloadResponse; 
          // Else try the network (nothing in the preloaded response):
          
          const networkResponse = await fetch(event.request);
          return networkResponse;
          // Else, we catch the error and show our offline page that supports auto reload, and once online the user will see the requested page:

        } catch (error) {
          // This 'catch' is only triggered if an exception is thrown, which is likely due to a network error.
          // If fetch() returns a valid HTTP response with a response code in the 4xx or 5xx range, the catch() will NOT be called.
          console.log('[Service Worker] Fetch failed; returning offline page instead.', error);          
          const cache = await caches.open(CACHE_NAME);
          const cachedResponse = await cache.match(OFFLINE_URL);
          return cachedResponse;
      }
    })());
  }

  // If our if() condition is false, then this fetch handler won't intercept the request. 
  // Other fetch handlers registered below will get a chance to call event.respondWith() and handle assets of the page differently.
  // If no fetch handlers call event.respondWith(), the request will be handled by the browser as if there were no service worker involvement.
  
  // If you don't need the cache at all, you should stop here and comment out any handlers below.
  // "While you can have more than one fetch event handler per service worker, only one can respond per request. For example,
  // you can have different handlers that will act on different URL patterns. They are executed in the order they were 
  // registered until one of them calls respondWith()." https://web.dev/learn/pwa/serving/#the-fetch-event
});


// ################################
// ####### CACHE STRATEGIES ####### 
// ################################

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

  // ########################################################################################
  // ####### 'Stale-while-revalidate' to keep up to date assets that we often editing #######
  // ########################################################################################
  // https://web.dev/learn/pwa/serving/#stale-while-revalidate
  // https://web.dev/offline-cookbook/#stale-while-revalidate
  // https://developer.chrome.com/docs/workbox/caching-strategies-overview/#stale-while-revalidate

  // We only want to update our assets each time, not Google fonts, etc...
  if (
      // Only our styles, all styles.
      (event.request.destination === 'style') && (event.request.url.includes('/assets/') || event.request.url.includes('/minify/'))
      
      // If we uncomment this line, then our JS files will be requested from the network every time the page is loaded.
      // This creates unnecessary network traffic if we don't edit these files often.
      // So we can decide to comment out this line and uncomment the corresponding line in the next 'else if'.
      // In this case, we need to increment the cache version number every time we change some of these JS files, because the next 'else if' will not update the cache.
      //|| (event.request.destination === 'script') && (event.request.url.includes('/assets/') || event.request.url.includes('/minify/'))
      
      // We want to update this on every page load because we edit it frequently.
      || event.request.url.includes('/assets/andy/js/script_v2.min.js')
     ) 
  {
    event.respondWith((async () => {
    try {
          const cache = await caches.open(CACHE_NAME);
          
          const cachedResponse = await cache.match(event.request);
          const fetchedResponse = await fetch(event.request);

          // Since each page will have resources from the /minify/ folder, we will always hit this first 'if' and
          // so this is the best place to take care of our offline resources.

          // Check if we still keep assets for the offline page:
          const CheckOfflineCache = await cache.match(OFFLINE_ASSETS_LOST_IMG);
          if (CheckOfflineCache === undefined) {
          // OFFLINE_ASSETS_LOST_IMG not found in cache, so we assume our offline assets are deleted.
          console.log('[Service Worker] assets for offline not found');
          // Add them again:
          cache.addAll(INITIAL_CACHED_RESOURCES);
          console.log('[Service Worker] Caching INITIAL_CACHED_RESOURCES again');
          } 
      
          // Update the cache with a clone of the network response.
          // "The Response's body is a ReadableStream that can only be consumed once" (https://web.dev/learn/pwa/serving/#stale-while-revalidate), so we must clone.
          cache.put(event.request, fetchedResponse.clone());
          
          // Prioritize cached response over network.
          // If the user has a cache from a previous visit, they will see cached assets on the first page load and updated on the second load.
          // '|| fetchedResponse" allows the user to see assets from the network if they don't have that asset in the cache yet.
          // So performance will be as good as for the cache-first if your app can tolerate one page load on old assets.
          return cachedResponse || fetchedResponse;
        
        } catch (error) {
          // Like in the 'Network-only' strategy we 'catch' error when an exception is thrown, which is likely due to a network error.
          // We need this to serve assets that match our 'if' condition from the cache, otherwise they will never be taken from there, 
          // because we promised to wait for the 'fetchedResponse' too and we would get 'Uncaught (in promise) TypeError: Failed to fetch'.
          console.log('[Service Worker] Fetch failed (Stale-while-revalidate); returning offline resources.', error);          
          const cache = await caches.open(CACHE_NAME);
          const cachedResponse = await cache.match(event.request);
          return cachedResponse;
          }
     })());
  }
  
  // ################################################################################################################################
  // ####### 'Cache-first' with 'Cache-as-you-browse' approach fast browsing experience and filling the cache with new assets #######
  // ################################################################################################################################
  // https://developer.chrome.com/docs/workbox/caching-strategies-overview/#cache-first-falling-back-to-network
  // https://web.dev/offline-cookbook/#cache-then-network
  
  // We still need to choose what we want to cache.
  // If you ever update these assets, you need to increment the cache version number!
  else if (
      // Don't put into the cache some odd stuff (see Network -> Initiator in DevTools).
      (!event.request.url.includes('google-analytics') && 
       !event.request.url.includes('browser-sync')  && 
       !event.request.url.includes('chrome-extension') && 
       !event.request.url.includes('twitter')) && 
    
      // Cache based on destinations (https://developer.mozilla.org/docs/Web/API/Request/destination) and URLs. 
      // Also exclude what we had in our first 'if'.

      // Caching images, only ours.
      event.request.url.includes('/media/') ||  ((event.request.destination === 'image') && event.request.url.includes('/assets/')) || 
     
      // Styles that are not ours (mainly to handle Google font caching).
      // We assume that all of our standalone styles were cached initially.
      ((event.request.destination === 'style') && (!event.request.url.includes('/assets/') || !event.request.url.includes('/minify/')))
      
      // If we uncomment this line, our JS files will never be updated.
      // This will only cache our JS files, not external ones (external ones will fall to the last 'else', where we do NOT cache anything).
      // If we use this (i.e. uncommented), we need to increment the cache version every time we change some of these JS files.
      // We also need to comment out the corresponding line in the 'if' above if we uncomment this one.
      || ((event.request.destination === 'script') && (event.request.url.includes('/assets/') || event.request.url.includes('/minify/') && !event.request.url.includes('/assets/andy/js/script_v2.min.js')))
    
      // Fonts that are not styles but fonts.
      || (event.request.destination === 'font')
    )
  {
    event.respondWith((async () => {
    try {
          const cache = await caches.open(CACHE_NAME);

          // Try the cache first:
          const cachedResponse = await cache.match(event.request);
          if (cachedResponse !== undefined) {
          // Found in cache, let's send the cached resource immideately:
          //console.log('[Service Worker] return cachedResponse');  // Heavily spamming to console.
          return cachedResponse;
          } 
          else {
          // Nothing in cach, try the network (and put to cache async):
          
          const fetchedResponse = await fetch(event.request);
          {
            // Save the new resource in the cache:
            //const ASSETS_TYPE = event.request.destination;
            //console.log('[Service Worker] ASSETS TYPE:', ASSETS_TYPE);  // Heavily spamming to console.
            cache.put(event.request, fetchedResponse.clone());
          }
          
          // And return it:
          //console.log('[Service Worker] return fetchedResponse'); // Heavily spamming to console.
          return fetchedResponse;
          }

        } catch (error) {
          // The same reason as described above.
          console.log('[Service Worker] Fetch failed (Cache-first && Cache-as-you-browse); returning offline resources.', error);          
          const cache = await caches.open(CACHE_NAME);
          const cachedResponse = await cache.match(event.request);
          return cachedResponse;
          }
    })());
  }
  
  // ################################################################
  // ####### 'Cache-first' without putting into the cache.   #######
  // ################################################################
  // A kind of default, for offline page mainly.
  // https://web.dev/learn/pwa/serving/#cache-first
  // https://microsoft.github.io/win-student-devs/#/30DaysOfPWA/core-concepts/05?id=_2-cache-first-on-fetch-retrieval

  // For other things, we use cache-first without putting it into the cache.
  // We need this because our offline page is also controlled by a service worker (many people don't realize this!).
  // The offline page is triggered when the catch in 'Network-only' is triggered, this will just be the HTML of this page.
  // Other assets on the offline page will pass our first 'if', then the second 'else if', but if they don't match the conditions?
  // Then they will not catch an error there, but will fall into this 'else', where we serve them from the cache. 
  // We may not have a 'cache-only' strategy here because when we are online, some other resources will go into this state and will get the
  // error 'The FetchEvent for ... resulted in a network error response: an object that was not a Response was passed to respondWith().'
  // Therefore, we must have network fallback for them.
  else
  {
    event.respondWith((async () => {
    try {
          const cache = await caches.open(CACHE_NAME);

          // Try the cache first:
          const cachedResponse = await cache.match(event.request);
          if (cachedResponse !== undefined) {
          // Found in cache, let's send the cached resource immideately:
          //console.log('[Service Worker] return cachedResponse');  // Heavily spamming to console.
          return cachedResponse;
          } 
          else {
            const fetchedResponse = await fetch(event.request);
            return fetchedResponse;
          }
        } catch (error) {
          // Almost the same as described for 'catch' after the first 'if' with the only difference being that here we have already provided
          // all possible assets from the cache, so we have that 'catch' here mainly to avoid 'Uncaught (in promise) TypeError: Failed to fetch'
          // errors. So we could only have the console log here and nothing else.
          console.log('[Service Worker] Fetch failed (Cache-first); returning offline resources.', error);          
          const cache = await caches.open(CACHE_NAME);
          const cachedResponse = await cache.match(event.request);
          return cachedResponse;
          }
    })());
  }
});

Collection of useful links you must read

Debug Progressive Web Apps (Google)

Debug Progressive Web Apps (PWAs) (Microsoft)

Tools and debug (Google)

Service workers (Very well presented information about service workers from Google)

Caching (Google)

Strategies for service worker caching (Very good article from Google's Workbox resource)

Read more Progressive Web App articles:

Posted