Workbox vs vite-pwa vs next-pwa: Service Workers & PWA in 2026
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
| Feature | Workbox | vite-pwa | next-pwa |
|---|---|---|---|
| Framework | None (universal) | Vite / SvelteKit / Nuxt / Astro | Next.js |
| Config | Manual | Zero-config | Minimal |
| Workbox strategies | ✅ All | ✅ Via config | ✅ Via workboxOptions |
| Background sync | ✅ | ✅ | ✅ |
| Custom SW file | ✅ | ✅ injectManifest | ✅ |
| Auto manifest gen | ❌ | ✅ | ❌ (manual) |
| Update prompts | Manual | ✅ React/Vue hooks | Manual |
| 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.