Skip to content


You must read Which Mode to Use before decide using this strategy on vite-plugin-pwa plugin.

Before writing your custom service worker, check if workbox can generate the code for you using generateSW strategy, looking for some plugin on workbox site on Runtime Caching Entry.

You can find the documentation for this method on workbox site: injectManifest


From version 0.15.0, vite-plugin-pwa builds your custom service worker using Vite instead of Rollup: configured Vite plugins were reused in the service worker build, which could lead to the generation of bad code in service worker.

If you are using any Vite plugin logic within your custom service worker, you need to add those plugins twice, for the development server and the build process:

  • Vite plugins
  • vite-plugin-pwa plugin options: injectManifest.plugins

vite-plugin-pwa now uses the same approach as Vite to build WebWorkers.

Exclude routes

To exclude some routes from being intercepted by the service worker, you just need to add those routes using a regex array to the denylist option of NavigationRoute:

import { createHandlerBoundToURL, precacheAndRoute } from 'workbox-precaching'
import { NavigationRoute, registerRoute } from 'workbox-routing'

declare let self: ServiceWorkerGlobalScope

// self.__WB_MANIFEST is default injection point

// to allow work offline
registerRoute(new NavigationRoute(
  { denylist: [/^\/backoffice/] },


You must deal with offline support for excluded routes: if requesting a page included on denylist you will get No internet connection.

Network First Strategy

You can use the following code to create your custom service worker to be used with network first strategy. We also include how to configure Custom Cache Network Race Strategy.

VitePWA options
  strategies: 'injectManifest',
  srcDir: 'src',
  filename: 'sw.ts'


You also need to add the logic to interact from the client logic: Advanced (injectManifest).

Then in your src/sw.ts file, remember you will also need to add following workbox dependencies as dev dependencies:

  • workbox-core
  • workbox-routing
  • workbox-strategies
  • workbox-build
import { cacheNames, clientsClaim } from 'workbox-core'
import { registerRoute, setCatchHandler, setDefaultHandler } from 'workbox-routing'
import type { StrategyHandler } from 'workbox-strategies'
import {
} from 'workbox-strategies'
import type { ManifestEntry } from 'workbox-build'

// Give TypeScript the correct global.
declare let self: ServiceWorkerGlobalScope
declare type ExtendableEvent = any

const data = {
  race: false,
  debug: false,
  credentials: 'same-origin',
  networkTimeoutSeconds: 0,
  fallback: 'index.html'

const cacheName = cacheNames.runtime

function buildStrategy(): Strategy {
  if (race) {
    class CacheNetworkRace extends Strategy {
      _handle(request: Request, handler: StrategyHandler): Promise<Response | undefined> {
        const fetchAndCachePutDone: Promise<Response> = handler.fetchAndCachePut(request)
        const cacheMatchDone: Promise<Response | undefined> = handler.cacheMatch(request)

        return new Promise((resolve, reject) => {
          fetchAndCachePutDone.then(resolve).catch((e) => {
            if (debug)
              console.log(`Cannot fetch resource: ${request.url}`, e)
          cacheMatchDone.then(response => response && resolve(response))

          // Reject if both network and cache error or find no response.
          Promise.allSettled([fetchAndCachePutDone, cacheMatchDone]).then((results) => {
            const [fetchAndCachePutResult, cacheMatchResult] = results
            if (fetchAndCachePutResult.status === 'rejected' && !cacheMatchResult.value)
    return new CacheNetworkRace()
  else {
    if (networkTimeoutSeconds > 0)
      return new NetworkFirst({ cacheName, networkTimeoutSeconds })
      return new NetworkFirst({ cacheName })

const manifest = self.__WB_MANIFEST as Array<ManifestEntry>

const cacheEntries: RequestInfo[] = []

const manifestURLs =
  (entry) => {
    const url = new URL(entry.url, self.location)
    cacheEntries.push(new Request(url.href, {
      credentials: credentials as any
    return url.href

self.addEventListener('install', (event: ExtendableEvent) => {
  event.waitUntil( => {
      return cache.addAll(cacheEntries)

self.addEventListener('activate', (event: ExtendableEvent) => {
  // - clean up outdated runtime cache
  event.waitUntil( => {
      // clean up those who are not listed in manifestURLs
      cache.keys().then((keys) => {
        keys.forEach((request) => {
          debug && console.log(`Checking cache entry to be removed: ${request.url}`)
          if (!manifestURLs.includes(request.url)) {
            cache.delete(request).then((deleted) => {
              if (debug) {
                if (deleted)
                  console.log(`Precached data removed: ${request.url || request}`)
                  console.log(`No precache found: ${request.url || request}`)

  ({ url }) => manifestURLs.includes(url.href),

setDefaultHandler(new NetworkOnly())

// fallback to app-shell for document request
setCatchHandler(({ event }): Promise<Response> => {
  switch (event.request.destination) {
    case 'document':
      return caches.match(fallback).then((r) => {
        return r ? Promise.resolve(r) : Promise.resolve(Response.error())
      return Promise.resolve(Response.error())

// this is necessary, since the new service worker will keep on skipWaiting state
// and then, caches will not be cleared since it is not activated

Server Push Notifications

You should check the workbox documentation: Introduction to push notifications.

You can check this awesome repo Elk using Server Push Notifications and some other cool service worker capabilities like Web Share Target API: using Nuxt 3 and vite-plugin-pwa.

Background Sync

You should check the workbox documentation: check Introducing to Background Sync.

You can check this awesome repo YT Playlist Notifier using Background Sync and some other cool service worker capabilities from the major collaborator of Workbox.

Released under the MIT License.