PWA With Vue and Vite: Building Offline and Installable Apps
AI generated
<v/>
{ }
Vue.js · PWA · Vite · Offline First
PWA with Vue and Vite: offline and installable
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.

18 min read Vue 3 · Vite · vite-plugin-pwa · Workbox Service Worker · Manifest · Caching

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.

11. FAQ: PWA With Vue and Vite

1What does a Vue app need to be a PWA?
A service worker, a web app manifest, and an HTTPS connection.
2Fastest PWA setup?
Via vite-plugin-pwa, which generates the service worker and manifest automatically from the Vite configuration.
3Caching strategy for API data?
Network first or stale while revalidate, depending on tolerance for briefly outdated data.
4Why old version after deployment?
The service worker only activates after all tabs are closed, unless active update handling with registerType prompt is used.
5Showing an update notice to users?
Via virtual:pwa-register and its onNeedRefresh callback for a banner or toast.
6Making structured data searchable offline?
Via IndexedDB, ideally with Dexie.js to simplify the native API.
7Installation the same on iOS as Android?
No, iOS Safari does not support beforeinstallprompt, only the manual path via the share menu.
8What is a maskable icon?
An icon with a safe zone so Android's cropping into different shapes does not cut off anything important.
9Should every app be a PWA?
Not necessarily, for deep native access needs Capacitor is often more fitting.
10Testing offline behavior?
Via devOptions.enabled in the dev server or the offline mode in Chrome DevTools.