Mattia
Mattia

Reputation: 140

Service Worker how to load index.html from cache only when offline, if online from network

I'm trying to implement a network strategy for index.html where it is always fetched from the network but falls back to the cache only when offline.

The reason for this approach is that my index.html contains the entry points for the JavaScript and CSS files, which include a hash in their filenames. When I push an update, these hashes change, meaning the service worker won't find the old .js and .css files in the cache. By always fetching index.html from the network, the app will instantly load the latest version without requiring a manual click to the reload prompt.

However, I still want the app to work offline, so if there's no internet connection, index.html should be served from the cache.

I'm using vite-plugin-pwa with injectManifest, and my current service worker caches everything, including HTML, CSS, JS, PNG, ICO, and SVG files.

Here’s my initial service worker setup:

/// <reference lib="webworker" />
import { cleanupOutdatedCaches, createHandlerBoundToURL, precacheAndRoute } from 'workbox-precaching';
import { NavigationRoute, registerRoute } from 'workbox-routing';

declare let self: ServiceWorkerGlobalScope;

self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'SKIP_WAITING') self.skipWaiting();
});

// self.__WB_MANIFEST is the default injection point
precacheAndRoute(self.__WB_MANIFEST);

// clean old assets
cleanupOutdatedCaches();

registerRoute(new NavigationRoute(createHandlerBoundToURL('index.html')));

I’ve been trying to modify it so that index.html is always fetched from the network, falling back to the cache only when offline. However, index.html still seems to be served from the cache every time.

Here’s what I’ve tried:

/// <reference lib="webworker" />
import { cleanupOutdatedCaches, createHandlerBoundToURL, precacheAndRoute } from 'workbox-precaching';
import { NavigationRoute, registerRoute } from 'workbox-routing';
import { NetworkFirst } from 'workbox-strategies';
import { cacheNames } from 'workbox-core';

declare let self: ServiceWorkerGlobalScope;

self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'SKIP_WAITING') self.skipWaiting();
});

// self.__WB_MANIFEST is the default injection point
precacheAndRoute(self.__WB_MANIFEST);

// clean old assets
cleanupOutdatedCaches();

// Retrieve the precache cache name dynamically
const precacheCacheName = cacheNames.precache;

registerRoute(
  ({ request }) => request.mode === 'navigate' || request.url.endsWith('/index.html'),
  new NetworkFirst({
    cacheName: precacheCacheName,
    plugins: [
      {
        fetchDidFail: async ({ request }) => {
          console.warn('Network request failed, serving from cache:', request.url);
        },
      },
    ],
  })
);

registerRoute(new NavigationRoute(createHandlerBoundToURL('index.html')));

What am I doing wrong?

Does my approach make sense fetching index.html from the network to always serve the latest version of the app?

By not caching the index.html, I eliminate the need for a reload prompt. The new service worker will activate once all tabs are closed. There's no need to call skipWaiting or reload the current page, as the content will already be updated with the new index.html.

Thanks

EDIT

I moved the registerRoute above precacheAndRoute(self.__WB_MANIFEST), and now it seems to be working. In the console (only if I have "Preserve log upon navigation" enabled in the DevTools preferences), I see the following logs:

Network request failed, serving from cache: https://example.com/
Serving from cache: https://example.com/
Navigated to https://example.com/

When I update my app, it correctly fetches the new index.html instead of the one from the cache.

However, I'm confused about why only the index.html is being served via a "network first" approach. I've added the condition request.mode === 'navigate' && request.url.endsWith('index.html'), but with this change, it always loads from the cache index.html and not the network first.

/// <reference lib="webworker" />
import { cleanupOutdatedCaches, createHandlerBoundToURL, precacheAndRoute } from 'workbox-precaching';
import { NavigationRoute, registerRoute } from 'workbox-routing';
import { NetworkFirst } from 'workbox-strategies';
import { cacheNames } from 'workbox-core';

declare let self: ServiceWorkerGlobalScope;

self.addEventListener('message', (event) => {
  if (event.data && event.data.type === 'SKIP_WAITING') self.skipWaiting();
});

// Retrieve the precache cache name dynamically
const precacheCacheName = cacheNames.precache;

registerRoute(
  ({ request }) => request.mode === 'navigate',
  new NetworkFirst({
    cacheName: precacheCacheName,
    plugins: [
      {
        fetchDidFail: async ({ request }) => {
          console.warn('Network request failed, serving from cache:', request.url);
          const cachedResponse = await caches.match(request);
          if (cachedResponse) {
            console.log('Serving from cache:', request.url);
          } else {
            console.error('Cache miss for:', request.url);
          }
        },
      },
    ],
  })
);

// self.__WB_MANIFEST is the default injection point
precacheAndRoute(self.__WB_MANIFEST);

// clean old assets
cleanupOutdatedCaches();

registerRoute(new NavigationRoute(createHandlerBoundToURL('index.html')));

EDIT 2

I almost solved it with ({ request, url }) => request.mode === 'navigate' && url.pathname === '/'.

The problem is now that on the very first page load precacheAndRoute(self.__WB_MANIFEST); is not caching / so it goes offline.

Upvotes: 0

Views: 41

Answers (0)

Related Questions