ring the build, and runtime routing during execution.
Step 1: Install the Build Integration
For Vite-based React applications, vite-plugin-pwa provides a streamlined wrapper around workbox-build. It handles service worker generation, registration injection, and manifest creation without requiring manual entry-point modifications.
npm install --save-dev vite-plugin-pwa
The plugin operates at build time, scanning the output directory and generating a complete service worker file. This eliminates the need to maintain a separate sw.ts file in source control. Registration is automatically injected into the application shell, ensuring the worker activates immediately after the first paint.
Step 2: Modularize Cache Strategy Configuration
Embedding all caching logic inside vite.config.ts creates configuration bloat. A cleaner architecture separates strategy definitions into a dedicated module. This approach improves readability, enables strategy reuse across environments, and simplifies testing.
Create src/pwa/cache-strategies.ts:
import type { PrecacheEntry, RouteOptions } from 'workbox-core';
export interface CacheStrategyConfig {
precacheEntries: PrecacheEntry[];
runtimeRoutes: RouteOptions[];
}
export function defineRuntimeStrategies(): RouteOptions[] {
return [
{
urlPattern: ({ request, url }) => {
return request.destination === 'image';
},
handler: 'CacheFirst',
options: {
cacheName: 'static-media-v1',
expiration: {
maxEntries: 150,
maxAgeSeconds: 2592000 // 30 days
},
cacheableResponse: {
statuses: [0, 200]
}
}
},
{
urlPattern: /^https:\/\/api\.platform\.example\.com\/v2\//,
handler: 'NetworkFirst',
options: {
cacheName: 'api-responses-v1',
expiration: {
maxEntries: 75,
maxAgeSeconds: 1800 // 30 minutes
},
networkTimeoutSeconds: 4
}
},
{
urlPattern: ({ url }) => url.pathname.startsWith('/docs/'),
handler: 'StaleWhileRevalidate',
options: {
cacheName: 'documentation-v1',
expiration: {
maxEntries: 50,
maxAgeSeconds: 604800 // 7 days
}
}
}
];
}
This module isolates routing logic. Each strategy targets a specific request pattern with explicit expiration boundaries. The networkTimeoutSeconds parameter prevents slow API calls from blocking the UI thread, falling back to cached data after 4 seconds. The cacheableResponse filter ensures only valid responses are stored, preventing opaque CORS errors from polluting the cache.
Step 3: Wire Configuration into Vite
Import the strategy module into the Vite configuration. The plugin merges precache manifests automatically, so only runtime routes and registration behavior require explicit declaration.
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { VitePWA } from 'vite-plugin-pwa';
import { defineRuntimeStrategies } from './src/pwa/cache-strategies';
export default defineConfig({
plugins: [
react(),
VitePWA({
registerType: 'prompt',
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'],
runtimeCaching: defineRuntimeStrategies(),
cleanupOutdatedCaches: true,
navigateFallback: '/index.html',
navigateFallbackDenylist: [/^\/api\//]
},
manifest: {
name: 'Platform Dashboard',
short_name: 'Dashboard',
description: 'Internal analytics and reporting interface',
theme_color: '#0f172a',
background_color: '#ffffff',
icons: [
{ src: '/assets/icons/icon-192.png', sizes: '192x192', type: 'image/png' },
{ src: '/assets/icons/icon-512.png', sizes: '512x512', type: 'image/png' }
]
}
})
]
});
Architecture decisions explained:
registerType: 'prompt' defers update activation until user consent. This prevents unexpected state resets in long-running dashboard sessions.
cleanupOutdatedCaches: true automatically removes stale precache entries from previous deployments, preventing storage quota exhaustion.
navigateFallback ensures client-side routing continues to function when the user navigates directly to a deep link while offline.
navigateFallbackDenylist excludes API routes from SPA fallback, preventing incorrect HTML responses from being cached for fetch requests.
Step 4: Implement Update Notification Logic
When using prompt registration, the application must listen for waiting service workers and expose a reload mechanism. The vite-plugin-pwa package exports a React hook for this purpose.
import { useRegisterSW } from 'virtual:pwa-register/react';
import { useState, useEffect } from 'react';
export function useUpdatePrompt() {
const {
needRefresh: [needRefresh],
updateServiceWorker
} = useRegisterSW({
onRegistered(r) {
r?.addEventListener('statechange', (e) => {
console.log(`Service Worker state: ${e.target?.state}`);
});
}
});
const [showBanner, setShowBanner] = useState(false);
useEffect(() => {
if (needRefresh) {
setShowBanner(true);
}
}, [needRefresh]);
const applyUpdate = () => {
updateServiceWorker(true);
setShowBanner(false);
};
return { showBanner, applyUpdate };
}
This hook isolates update state management from UI components. The updateServiceWorker(true) call triggers skipWaiting() on the waiting worker, forcing immediate activation. The banner dismissal prevents UI clutter while preserving the update path.
Step 5: Verify Production Behavior
Service workers are intentionally disabled in Vite's development server to prevent interference with Hot Module Replacement. Validation must occur against a production build.
npm run build
npm run preview
Open Chrome DevTools β Application tab. Verify the following:
- Service Worker registration shows
activated status.
- Cache Storage contains
precache-manifest-* with content-hashed entries.
- Runtime caches (
static-media-v1, api-responses-v1) populate on first request.
- Network throttling to
Offline serves precached assets and falls back to runtime cache for API calls.
Run Lighthouse PWA audit against the preview URL. The report validates installability, service worker lifecycle compliance, and offline readiness. Address any fail items before deployment.
Pitfall Guide
1. Unbounded Cache Growth
Explanation: Runtime caches without expiration limits accumulate indefinitely. Browsers enforce storage quotas per origin; exceeding them triggers silent cache eviction, breaking offline functionality.
Fix: Always configure expiration.maxEntries and expiration.maxAgeSeconds for every runtime route. Monitor cache size during QA using DevTools Storage panel.
2. Testing Service Workers in Development Mode
Explanation: Vite disables service workers by default during npm run dev. Developers often assume caching works identically in dev and production, leading to false confidence.
Fix: Validate all offline behavior against npm run build && npm run preview. Use registerType: 'autoUpdate' in a staging environment to simulate production updates without manual prompts.
3. Ignoring TypeScript WebWorker Scope
Explanation: Service workers execute in a separate global scope (self) with restricted APIs. Standard lib.dom.d.ts types cause compilation errors for FetchEvent, CacheStorage, and clients.
Fix: When using InjectManifest mode, create a dedicated tsconfig.sw.json with "lib": ["ES2020", "WebWorker"]. Reference it via /// <reference lib="webworker" /> at the top of custom worker files.
4. Blocking the Main Thread with Registration Logic
Explanation: Synchronous service worker registration or heavy initialization logic delays first contentful paint. Users on low-end devices experience noticeable UI jank.
Fix: Defer registration using requestIdleCallback or dynamic imports. Keep the registration hook lightweight. Avoid fetching configuration data inside the service worker install event.
5. Misconfiguring Update Prompts
Explanation: Showing update banners on every page load or failing to dismiss them after activation causes infinite reload loops and user frustration.
Fix: Track prompt state in application memory or sessionStorage. Only display the banner when needRefresh transitions from false to true. Ensure updateServiceWorker(true) is called exactly once per update cycle.
6. Over-Caching Critical API Responses
Explanation: Applying CacheFirst or long TTLs to authentication endpoints, real-time dashboards, or transactional APIs serves stale data, causing state desynchronization.
Fix: Reserve NetworkFirst or NetworkOnly for sensitive endpoints. Implement short TTLs (5-15 minutes) for semi-static data. Use cache-busting query parameters for critical fetches.
7. Assuming Opaque Responses are Cacheable
Explanation: Cross-origin requests without proper CORS headers return opaque responses. Workbox rejects these by default, causing silent cache misses and broken offline fallbacks.
Fix: Configure cacheableResponse: { statuses: [0, 200] } only when the third-party server explicitly supports CORS. Otherwise, route external assets through a proxy or exclude them from caching.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Long-running dashboard with real-time data | NetworkFirst with 5-min TTL | Prevents stale metrics while providing offline resilience | Low bandwidth, moderate storage |
| Static documentation or marketing pages | StaleWhileRevalidate | Instant load with background refresh for freshness | Minimal storage, high perceived performance |
| Third-party image CDN without CORS | Exclude from cache or use proxy | Opaque responses cannot be stored or replayed reliably | Higher network cost, avoids silent failures |
| Internal tool with controlled deployments | autoUpdate registration | Eliminates user friction during frequent internal releases | Zero UI overhead, immediate consistency |
| Customer-facing SaaS with session state | prompt registration with banner | Prevents unexpected state resets during active workflows | Slight UI complexity, high user trust |
Configuration Template
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { VitePWA } from 'vite-plugin-pwa';
import { defineRuntimeStrategies } from './src/pwa/cache-strategies';
export default defineConfig({
plugins: [
react(),
VitePWA({
registerType: 'prompt',
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'],
runtimeCaching: defineRuntimeStrategies(),
cleanupOutdatedCaches: true,
navigateFallback: '/index.html',
navigateFallbackDenylist: [/^\/api\//, /^\/auth\//],
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024
},
manifest: {
name: 'Platform Dashboard',
short_name: 'Dashboard',
description: 'Internal analytics and reporting interface',
theme_color: '#0f172a',
background_color: '#ffffff',
icons: [
{ src: '/assets/icons/icon-192.png', sizes: '192x192', type: 'image/png' },
{ src: '/assets/icons/icon-512.png', sizes: '512x512', type: 'image/png' }
]
}
})
]
});
Quick Start Guide
- Initialize the plugin: Run
npm install --save-dev vite-plugin-pwa in your React project root.
- Create strategy module: Add
src/pwa/cache-strategies.ts and define your runtime routes using the RouteOptions interface.
- Wire into Vite: Import the strategy module into
vite.config.ts and configure registerType, globPatterns, and manifest metadata.
- Build and preview: Execute
npm run build && npm run preview, then verify service worker activation and cache population in DevTools.
- Test offline resilience: Throttle network to
Offline in DevTools, navigate to deep routes, and confirm precached assets and runtime fallbacks load correctly.