service worker, manifest, and caching without the app store detour
A PWA with Vue and Vite turns an existing web application into an installable, offline capable app, without app store review and without a separate native codebase. With vite-plugin-pwa, service worker, web app manifest, and caching strategies can be configured largely automated, while update handling and offline behavior get tailored specifically to the application at hand.
Table of Contents
- 1. What sets a PWA apart from a regular Vue application
- 2. Project setup with vite-plugin-pwa
- 3. The web app manifest: controlling installability
- 4. Caching strategies: cache first, network first, stale while revalidate
- 5. Designing offline behavior deliberately instead of leaving it to chance
- 6. Update handling: shipping new versions without confusing users
- 7. Making API data available offline with IndexedDB
- 8. Install prompts and user guidance
- 9. PWA versus Capacitor for native distribution
- 10. Summary
- 11. FAQ
1. What sets a PWA apart from a regular Vue application
A PWA with Vue differs from a regular web application through three technical additions: a service worker that can intercept and cache network requests, a web app manifest that provides the operating system with metadata for installation, and an HTTPS connection, which is a strict requirement for service workers. Together, these three building blocks allow a Vue application to be installed to the home screen and to keep working, at least partially, even without a network connection.
The decisive difference from a classic web application lies in the service worker acting as an independent JavaScript context, separate from the main thread, sitting between the network and the application. Every request from the Vue PWA can be intercepted by the service worker before it even reaches the network, enabling targeted caching decisions: serving static assets from the cache, giving API responses a fallback to cached data, or answering entirely offline from the cache.
For Vue developers, the pragmatic path into a PWA with Vue is considerably easier today than a few years ago, because vite-plugin-pwa automates the creation of the manifest and service worker and builds on Google's proven Workbox library. Instead of writing a service worker from scratch, caching strategies are configured declaratively in the Vite configuration, and the plugin generates the matching service worker code on every build.
2. Project setup with vite-plugin-pwa
Getting started with a PWA with Vue begins with installing vite-plugin-pwa into an existing Vue 3 project built with Vite. The plugin gets registered in vite.config.js and from that point on automatically takes over generating the manifest file and service worker on every production build, without those files needing to be maintained by hand.
For development, the devOptions.enabled option is especially relevant, activating the service worker even in the local Vite dev server. Without this option, offline behavior is hard to test during development, because service workers are only registered in production builds by default.
# Add the PWA plugin to an existing Vue 3 + Vite project
npm install -D vite-plugin-pwa
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { VitePWA } from 'vite-plugin-pwa'
export default defineConfig({
plugins: [
vue(),
VitePWA({
registerType: 'prompt', // let the user decide when to activate updates
devOptions: {
enabled: true, // test the service worker during `vite dev`
},
manifest: {
name: 'Mironsoft App',
short_name: 'Mironsoft',
theme_color: '#16a34a',
background_color: '#0f172a',
display: 'standalone',
icons: [
{ src: 'icon-192.png', sizes: '192x192', type: 'image/png' },
{ src: 'icon-512.png', sizes: '512x512', type: 'image/png' },
],
},
workbox: {
globPatterns: ['**/*.{js,css,html,svg,png,webp}'],
},
}),
],
})
After the build, vite-plugin-pwa automatically produces a manifest.webmanifest file as well as a service worker bundle in the dist directory, both already linked correctly in the HTML head. For most standard use cases of a PWA with Vue, this base configuration is already enough to satisfy Lighthouse requirements for installability.
3. The web app manifest: controlling installability
The web app manifest is a JSON file that tells the operating system how a Vue PWA should look and behave after installation. Fields such as name, icons, and start_url are mandatory for installability, while display: standalone ensures the app opens without browser chrome such as the address bar and tabs, considerably improving the native appearance.
A frequently overlooked detail is maskable icons: Android crops app icons into different shapes depending on the manufacturer skin, round, rounded square, or teardrop. An icon with the addition purpose: maskable in the manifest signals to the operating system that enough safe zone exists around the edge so important visual elements do not get cut off.
{
"name": "Mironsoft App",
"short_name": "Mironsoft",
"description": "Product catalog and customer management",
"start_url": "/?source=pwa",
"display": "standalone",
"theme_color": "#16a34a",
"background_color": "#0f172a",
"orientation": "portrait",
"icons": [
{ "src": "icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "icon-512.png", "sizes": "512x512", "type": "image/png" },
{ "src": "icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
]
}
The start_url field with the query parameter ?source=pwa is a useful pattern for later distinguishing in analytics whether a page view originated from the installed Vue PWA or from the regular browser. This information is valuable for understanding how much the installed app is actually used compared to the browser version.
4. Caching strategies: cache first, network first, stale while revalidate
The core of every PWA with Vue is deciding which caching strategy makes sense for which kind of resource. Static assets such as CSS, JavaScript, and images only change on a new deployment and are therefore well suited to the cache first strategy: the service worker serves the resource directly from the cache without even starting a network request, reducing both load time and data usage.
For API responses that change more frequently, network first or stale while revalidate is usually more fitting. Network first tries the network first and only falls back to the cache on failure, prioritizing fresh data but causing wait time on a slow connection. Stale while revalidate immediately serves the cached version and updates the cache in the background for the next call, representing a good compromise between speed and freshness.
// vite.config.js — workbox runtime caching configuration
VitePWA({
workbox: {
runtimeCaching: [
{
// Static assets: served instantly, never re-fetched until a new build
urlPattern: /\.(?:png|jpg|jpeg|svg|webp|woff2)$/,
handler: 'CacheFirst',
options: {
cacheName: 'static-assets',
expiration: { maxEntries: 100, maxAgeSeconds: 60 * 60 * 24 * 30 },
},
},
{
// Product API: prefer fresh data, but survive brief network drops
urlPattern: /^https:\/\/api\.mironsoft\.de\/products/,
handler: 'NetworkFirst',
options: {
cacheName: 'products-api',
networkTimeoutSeconds: 3,
expiration: { maxEntries: 50, maxAgeSeconds: 60 * 60 },
},
},
{
// Dashboard stats: show cached data instantly, refresh silently
urlPattern: /^https:\/\/api\.mironsoft\.de\/dashboard/,
handler: 'StaleWhileRevalidate',
options: { cacheName: 'dashboard-api' },
},
],
},
})
Choosing the right strategy is not a purely technical decision, it depends on the business context. A price in an online shop should never be served with cache first, because outdated prices can lead to order mistakes. A static company logo, by contrast, practically never changes and benefits maximally from aggressive caching. This business classification should come before the technical configuration of every Vue PWA.
5. Designing offline behavior deliberately instead of leaving it to chance
A PWA with Vue without carefully designed offline behavior shows, at best, a generic browser error page, at worst an empty, confusing view. A dedicated offline fallback page, served by the service worker as soon as neither the network nor the cache can provide an answer, significantly improves the user experience and clearly explains that there is currently no connection.
It is also worth accounting for the online status inside the Vue Router itself, for example via the navigator.onLine property combined with the browser's online and offline events. A global status indicator that reactively reflects the connection status prevents users from attempting actions that will inevitably fail because there is no network connection.
// composables/useOnlineStatus.js
import { ref, onMounted, onUnmounted } from 'vue'
export function useOnlineStatus() {
const isOnline = ref(navigator.onLine)
function updateStatus() {
isOnline.value = navigator.onLine
}
onMounted(() => {
window.addEventListener('online', updateStatus)
window.addEventListener('offline', updateStatus)
})
onUnmounted(() => {
window.removeEventListener('online', updateStatus)
window.removeEventListener('offline', updateStatus)
})
return { isOnline }
}
An important aspect for Vue PWA applications with forms: instead of simply acknowledging a failed request with an error message, a background sync strategy can buffer the request and automatically resend it once the connection returns. This requires additional logic through the Workbox Background Sync API, but significantly improves the user experience on unstable connections.
6. Update handling: shipping new versions without confusing users
An often underestimated problem with a PWA with Vue: the service worker caches a version of the application so effectively that users may see the old version for days after a deployment, because the new service worker installs in the background but only activates once every tab has been fully closed. Without active update handling, this delay stays invisible and confusing to users.
The registerType: 'prompt' option registered in section two hands control back to the application: instead of automatically updating the service worker, vite-plugin-pwa provides a virtual module through which the Vue application detects when a new version is available and can actively show the user a notice before the update gets triggered.
// composables/usePwaUpdate.js
import { ref } from 'vue'
import { registerSW } from 'virtual:pwa-register'
export function usePwaUpdate() {
const updateAvailable = ref(false)
let updateSW
updateSW = registerSW({
onNeedRefresh() {
updateAvailable.value = true // show a toast/banner in the UI
},
onOfflineReady() {
console.log('App ready to work offline')
},
})
function applyUpdate() {
updateSW(true) // reloads the page with the new service worker active
}
return { updateAvailable, applyUpdate }
}
In practice, when updateAvailable === true, a subtle banner is shown with text like "New version available" and a button that triggers applyUpdate. This pattern ensures users of a Vue PWA are not unexpectedly interrupted during an ongoing input, but instead decide themselves when the reload for the new version happens.
7. Making API data available offline with IndexedDB
Plain HTTP caching through the service worker is sufficient for simple, repeatable GET requests, but reaches its limits once a Vue PWA needs to make structured data searchable or filterable offline. IndexedDB is the browser database of choice for such cases, because unlike localStorage it supports larger data volumes, indexes for queries, and asynchronous access.
The Dexie.js library considerably simplifies working with IndexedDB compared to the raw, rather cumbersome native API. Combined with a Pinia store, a pattern can be established where data is loaded from IndexedDB first, while a fresh version is fetched from the server in the background and the local dataset gets updated.
// stores/useProductStore.js — Pinia store backed by IndexedDB via Dexie
import { defineStore } from 'pinia'
import Dexie from 'dexie'
const db = new Dexie('mironsoft-db')
db.version(1).stores({ products: 'id, name, category' })
export const useProductStore = defineStore('products', {
state: () => ({ products: [] }),
actions: {
async loadProducts() {
// Show cached data immediately, even fully offline
this.products = await db.products.toArray()
try {
const response = await fetch('https://api.mironsoft.de/products')
const fresh = await response.json()
this.products = fresh
await db.products.clear()
await db.products.bulkPut(fresh) // persist for the next offline session
} catch {
// Network failed, the IndexedDB snapshot from above stays in use
}
},
},
})
8. Install prompts and user guidance
Chrome and Edge fire the beforeinstallprompt event once their internal heuristics classify a Vue PWA as worth installing, usually after repeated visits and a valid manifest along with a service worker. This event can be intercepted to offer a custom, design integrated install button instead of relying on the browser's own install hint, which is easily overlooked.
Safari on iOS does not support this event and instead only offers the manual path via Add to Home Screen in the share menu. For iOS users, a subtle, context sensitive guide explaining exactly this path pays off, because without a hint, many users simply never discover the installation option on iOS.
// composables/useInstallPrompt.js
import { ref, onMounted } from 'vue'
export function useInstallPrompt() {
const deferredPrompt = ref(null)
const canInstall = ref(false)
onMounted(() => {
window.addEventListener('beforeinstallprompt', (event) => {
event.preventDefault() // suppress the browser's default mini-infobar
deferredPrompt.value = event
canInstall.value = true
})
})
async function promptInstall() {
if (!deferredPrompt.value) return
deferredPrompt.value.prompt()
const { outcome } = await deferredPrompt.value.userChoice
canInstall.value = false
deferredPrompt.value = null
return outcome // 'accepted' or 'dismissed'
}
return { canInstall, promptInstall }
}
9. PWA versus Capacitor for native distribution
A PWA with Vue is not always the right choice for every project. The decision between a PWA and a native wrapper like Capacitor depends heavily on which distribution channels and which native functionality are actually needed.
| Criterion | PWA with Vue | Vue with Capacitor |
|---|---|---|
| Distribution | Direct via URL, no store review | App Store and Play Store, review required |
| Update speed | Immediately after deployment | Store review time for native updates |
| Native APIs | Limited to web APIs | Full access via plugins |
| iOS installability | Manual only, via share menu | Regular app store listing |
| Development effort | Minimal, same codebase | Additional native build process |
For internal business tools, content heavy applications, or projects with a limited budget, a PWA with Vue is often the faster and cheaper solution, because neither store fees nor review processes apply and updates reach every user immediately. However, once a project depends on store presence for marketing purposes or needs deep access to native hardware functionality, Capacitor is the more fitting choice, as described in the first article of this series.
Mironsoft
Vue development, progressive web apps, and offline architecture
Want your Vue application installable and offline capable?
We set up service worker, manifest, and caching strategies for your Vue application, configure clean update handling, and ensure a deliberate offline experience instead of random behavior.
PWA setup
Configuring vite-plugin-pwa, providing manifest and icons
Caching strategy
Defining the right strategy per resource type, technically and business wise
Offline UX
Implementing fallback pages, status indicators, and update banners
10. Summary
A PWA with Vue turns an existing Vue application into an installable, offline capable app with comparatively little effort. vite-plugin-pwa automates the generation of the service worker and web app manifest, while Workbox based caching strategies such as cache first, network first, and stale while revalidate should be configured deliberately per resource type, instead of using a single strategy for everything.
Deliberate offline behavior with a dedicated fallback page, active update handling with user notification instead of silent updates, and IndexedDB for offline searchable data lift a Vue PWA from a mere technical exercise to a genuinely usable offline experience. Compared to Capacitor, the PWA remains the faster, store free alternative for projects without a need for deep native functional access.
PWA with Vue and Vite — the essentials at a glance
Setup
vite-plugin-pwa automatically generates service worker and manifest on every build.
Caching strategies
Cache first for static assets, network first or stale while revalidate for API data.
Update handling
registerType: prompt gives the app control over when the update happens.
Offline data
IndexedDB via Dexie.js for structured, offline searchable API data.