Skip to main content

Workbox vs vite-pwa vs next-pwa: Service Workers & PWA in 2026

·PkgPulse Team

TL;DR

Workbox (from Google) is the foundational service worker toolkit — provides caching strategies, background sync, precaching, and push notification helpers. It's the layer that vite-pwa and next-pwa build on top of. vite-pwa is the zero-config PWA plugin for Vite (and SvelteKit, Nuxt, Astro, SolidStart) — generates service workers using Workbox with minimal setup. next-pwa wraps Workbox specifically for Next.js with automatic route precaching. In 2026: pick the wrapper for your framework (vite-pwa for Vite projects, next-pwa for Next.js), reach for raw Workbox only when you need custom service worker logic beyond what the wrappers provide.

Key Takeaways

  • workbox: ~5M weekly downloads — Google's service worker toolkit, all caching strategies
  • vite-pwa (vite-plugin-pwa): ~2M weekly downloads — zero-config PWA for Vite ecosystem
  • next-pwa: ~300K weekly downloads — Workbox wrapper for Next.js
  • Service workers intercept network requests — use them for offline support, caching, and push notifications
  • Workbox strategies: CacheFirst, NetworkFirst, StaleWhileRevalidate, NetworkOnly, CacheOnly
  • PWA requirements: HTTPS + service worker + Web App Manifest → "Add to Home Screen" prompts

Service Worker Fundamentals

Service Worker lifecycle:
  1. register()  — browser downloads and parses the SW file
  2. install     — SW caches precached assets (AppShell)
  3. activate    — old SW replaced, new SW takes control
  4. fetch       — SW intercepts all network requests

Caching strategies:
  CacheFirst       — serve from cache, fetch in background if cache miss
  NetworkFirst     — try network, fall back to cache (freshest data)
  StaleWhileRevalidate — serve cached immediately, update cache in background
  NetworkOnly      — always fetch (for dynamic APIs)
  CacheOnly        — cache only (offline-first assets)

Use cases:
  Static assets (JS, CSS, images)  → CacheFirst (hash in filename = safe)
  HTML pages                        → NetworkFirst (ensure latest layout)
  API responses                     → StaleWhileRevalidate (fast + fresh)
  Real-time data (prices, chat)     → NetworkOnly (no stale data)

Workbox

Workbox — Google's service worker libraries:

Service worker setup (manual Workbox)

// service-worker.js — imported by your app, runs in SW context

import { precacheAndRoute } from "workbox-precaching"
import { registerRoute, NavigationRoute } from "workbox-routing"
import {
  CacheFirst,
  NetworkFirst,
  StaleWhileRevalidate,
} from "workbox-strategies"
import { ExpirationPlugin } from "workbox-expiration"
import { BackgroundSyncPlugin } from "workbox-background-sync"

// Precache assets (injected by Workbox build tools during build):
precacheAndRoute(self.__WB_MANIFEST)
// Workbox injects the manifest at build time:
// [{ url: "/app.abc123.js", revision: null }, { url: "/index.html", revision: "def456" }]

// Cache images with CacheFirst + expiration:
registerRoute(
  ({ request }) => request.destination === "image",
  new CacheFirst({
    cacheName: "images-cache",
    plugins: [
      new ExpirationPlugin({
        maxEntries: 100,          // Max 100 images in cache
        maxAgeSeconds: 30 * 24 * 60 * 60,  // 30 days
        purgeOnQuotaError: true,
      }),
    ],
  })
)

// Cache API responses with StaleWhileRevalidate:
registerRoute(
  ({ url }) => url.pathname.startsWith("/api/packages"),
  new StaleWhileRevalidate({
    cacheName: "api-packages-cache",
    plugins: [
      new ExpirationPlugin({
        maxEntries: 50,
        maxAgeSeconds: 5 * 60,  // 5 minutes
      }),
    ],
  })
)

// NetworkFirst for HTML navigation (always try to get latest page):
registerRoute(
  new NavigationRoute(
    new NetworkFirst({
      cacheName: "pages-cache",
      networkTimeoutSeconds: 3,  // Fall back to cache after 3s timeout
    })
  )
)

Background sync

import { BackgroundSyncPlugin } from "workbox-background-sync"
import { registerRoute } from "workbox-routing"
import { NetworkOnly } from "workbox-strategies"

// Retry failed POST requests when back online:
const bgSyncPlugin = new BackgroundSyncPlugin("analytics-queue", {
  maxRetentionTime: 24 * 60,  // Retry for up to 24 hours (in minutes)
})

registerRoute(
  ({ url }) => url.pathname === "/api/analytics",
  new NetworkOnly({
    plugins: [bgSyncPlugin],
  }),
  "POST"
)
// If the POST fails (offline), it's stored and retried when connectivity returns

Register from the app

// src/registerSW.ts:
import { Workbox } from "workbox-window"

const wb = new Workbox("/service-worker.js")

// Show update notification:
wb.addEventListener("waiting", () => {
  if (confirm("New version available. Reload?")) {
    wb.messageSkipWaiting()
    wb.addEventListener("controlling", () => window.location.reload())
  }
})

wb.register()

vite-pwa (vite-plugin-pwa)

vite-pwa — zero-config PWA for Vite, SvelteKit, Nuxt, Astro:

Vite setup

// vite.config.ts:
import { defineConfig } from "vite"
import react from "@vitejs/plugin-react"
import { VitePWA } from "vite-plugin-pwa"

export default defineConfig({
  plugins: [
    react(),
    VitePWA({
      registerType: "autoUpdate",  // Auto-update SW without prompt

      // Web App Manifest:
      manifest: {
        name: "PkgPulse",
        short_name: "PkgPulse",
        description: "npm package health scores and comparisons",
        theme_color: "#0a0a0a",
        background_color: "#0a0a0a",
        display: "standalone",
        scope: "/",
        start_url: "/",
        icons: [
          { src: "/icons/icon-192.png", sizes: "192x192", type: "image/png" },
          { src: "/icons/icon-512.png", sizes: "512x512", type: "image/png" },
          { src: "/icons/icon-512.png", sizes: "512x512", type: "image/png", purpose: "maskable" },
        ],
      },

      // Workbox configuration:
      workbox: {
        // Precache all static assets (Vite generates hashed filenames):
        globPatterns: ["**/*.{js,css,html,ico,png,svg,woff2}"],

        // Runtime caching strategies:
        runtimeCaching: [
          {
            urlPattern: /^https:\/\/api\.pkgpulse\.com\/packages\/.*/i,
            handler: "StaleWhileRevalidate",
            options: {
              cacheName: "api-packages-cache",
              expiration: { maxEntries: 100, maxAgeSeconds: 5 * 60 },
            },
          },
          {
            urlPattern: /\.(?:png|jpg|jpeg|svg|gif|webp)$/,
            handler: "CacheFirst",
            options: {
              cacheName: "images-cache",
              expiration: { maxEntries: 200, maxAgeSeconds: 30 * 24 * 60 * 60 },
            },
          },
        ],
      },
    }),
  ],
})

Prompt for update

// vite.config.ts — use "prompt" instead of "autoUpdate":
VitePWA({
  registerType: "prompt",

  // ... manifest and workbox config
})
// src/UpdatePrompt.tsx — show update notification:
import { useRegisterSW } from "virtual:pwa-register/react"

export function UpdatePrompt() {
  const {
    needRefresh: [needRefresh, setNeedRefresh],
    updateServiceWorker,
  } = useRegisterSW({
    onRegistered(r) {
      console.log("SW Registered:", r)
    },
    onRegisterError(error) {
      console.log("SW registration error", error)
    },
  })

  if (!needRefresh) return null

  return (
    <div className="update-prompt">
      <span>New content available, click on reload button to update.</span>
      <button onClick={() => updateServiceWorker(true)}>Reload</button>
      <button onClick={() => setNeedRefresh(false)}>Close</button>
    </div>
  )
}

SvelteKit integration

// vite.config.ts (SvelteKit):
import { sveltekit } from "@sveltejs/kit/vite"
import { VitePWA } from "vite-plugin-pwa"

export default {
  plugins: [
    sveltekit(),
    VitePWA({
      srcDir: "src",
      filename: "service-worker.ts",
      strategies: "injectManifest",  // Use custom SW file
      manifest: { /* ... */ },
    }),
  ],
}

next-pwa

@ducanh2912/next-pwa — PWA for Next.js:

Setup

// next.config.js:
const withPWA = require("@ducanh2912/next-pwa").default({
  dest: "public",                  // Service worker output directory
  cacheOnFrontEndNav: true,        // Cache pages on client-side navigation
  aggressiveFrontEndNavCaching: true,
  reloadOnOnline: true,            // Reload when back online after offline
  swcMinify: true,
  disable: process.env.NODE_ENV === "development",  // No SW in dev

  workboxOptions: {
    disableDevLogs: true,
    // Custom runtime caching:
    runtimeCaching: [
      {
        urlPattern: /^https:\/\/fonts\.(?:gstatic)\.com\/.*/i,
        handler: "CacheFirst",
        options: {
          cacheName: "google-fonts-webfonts",
          expiration: {
            maxEntries: 4,
            maxAgeSeconds: 365 * 24 * 60 * 60,  // 1 year
          },
        },
      },
      {
        urlPattern: /\/api\/packages\/.*/i,
        handler: "NetworkFirst",
        options: {
          cacheName: "api-packages",
          networkTimeoutSeconds: 10,
          expiration: { maxEntries: 32, maxAgeSeconds: 24 * 60 * 60 },
        },
      },
    ],
  },
})

module.exports = withPWA({
  // Your existing next.config.js:
  reactStrictMode: true,
})

Web App Manifest for Next.js

// public/manifest.json:
{
  "name": "PkgPulse",
  "short_name": "PkgPulse",
  "description": "npm package health scores",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#0a0a0a",
  "theme_color": "#0a0a0a",
  "icons": [
    { "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
    { "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" }
  ]
}
// app/layout.tsx — link manifest:
import type { Metadata } from "next"

export const metadata: Metadata = {
  manifest: "/manifest.json",
  themeColor: "#0a0a0a",
  appleWebApp: {
    capable: true,
    statusBarStyle: "black-translucent",
    title: "PkgPulse",
  },
}

Feature Comparison

FeatureWorkboxvite-pwanext-pwa
FrameworkNone (universal)Vite / SvelteKit / Nuxt / AstroNext.js
ConfigManualZero-configMinimal
Workbox strategies✅ All✅ Via config✅ Via workboxOptions
Background sync
Custom SW file✅ injectManifest
Auto manifest gen❌ (manual)
Update promptsManual✅ React/Vue hooksManual
Weekly downloads~5M~2M~300K
TypeScript

When to Use Each

Choose vite-pwa if:

  • Using Vite, SvelteKit, Nuxt 3+, or Astro
  • Want zero-config setup with Workbox under the hood
  • Need framework-specific update hooks (useRegisterSW)

Choose next-pwa if:

  • Using Next.js (App Router or Pages Router)
  • Want automatic Next.js route precaching
  • Minimal config with sensible defaults

Use raw Workbox directly if:

  • Non-Vite, non-Next.js projects (Webpack, Parcel)
  • Need fine-grained control over the service worker
  • Building a custom service worker with complex routing logic
  • Working on a library or component that ships its own service worker

Skip PWA/service workers if:

  • Server-side only app (no meaningful offline state)
  • Auth-heavy apps where stale data is a security risk
  • App is always live with no valuable offline experience

Methodology

Download data from npm registry (weekly average, February 2026). Feature comparison based on workbox v7.x, vite-plugin-pwa v0.x, and @ducanh2912/next-pwa v10.x.

Compare frontend tooling and build packages on PkgPulse →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.