Skip to content
ExponaBLOG
VUE.JS & NUXT · DAY 066 / 100

PWA with Vue and Nuxt: a Vue & Nuxt guide for developers

Installable, offline-capable apps.

Expona Admin7 min read

PWA with Vue and Nuxt: a Vue & Nuxt guide for developers

Building a Progressive Web App (PWA) with Vue and Nuxt transforms your web application into something that feels native—installable on home screens, functional offline, and fast even on slow connections. We've all felt the friction of web apps that depend entirely on network availability; PWAs eliminate that friction by bringing service workers, caching strategies, and install prompts into your Vue and Nuxt stack.

In this guide, we'll walk through the practical steps to turn a Nuxt application into a fully-featured PWA, from plugin setup through intelligent caching and update management.

Understanding PWA Essentials for Vue and Nuxt Applications

A PWA combines three core technologies: a manifest file that describes your app, a service worker that intercepts network requests, and HTTPS (non-negotiable in production). For Nuxt developers, the good news is that most of this complexity is abstracted by @vite-pwa/nuxt, a first-class integration that handles service worker generation, manifest creation, and asset precaching automatically.

Before diving into implementation, understand that PWAs exist on a spectrum. You can have a minimal PWA with just offline support for critical assets, or a sophisticated one with background sync and push notifications. We'll focus on the core: installation, offline capability, and update prompts.

Plugin Setup: @vite-pwa/nuxt for Production-Ready PWAs

The cleanest way to add PWA support to Nuxt 3 is through @vite-pwa/nuxt. This plugin integrates with Vite, auto-generates your service worker, and manages precaching without boilerplate.

Installation and basic configuration:

typescript
npm install -D @vite-pwa/nuxt workbox-window

Add the module to your nuxt.config.ts:

typescript
export default defineNuxtConfig({
  modules: ['@vite-pwa/nuxt'],
  pwa: {
    strategies: 'auto',
    workbox: {
      globPatterns: ['**/*.{js,css,html,png,svg,ico}'],
    },
    manifest: {
      name: 'My E-Commerce Store',
      short_name: 'Store',
      description: 'Fast, offline-capable shopping experience',
      theme_color: '#ffffff',
      background_color: '#ffffff',
      display: 'standalone',
      scope: '/',
      start_url: '/',
      icons: [
        {
          src: '/icon-192.png',
          sizes: '192x192',
          type: 'image/png',
          purpose: 'any',
        },
        {
          src: '/icon-512.png',
          sizes: '512x512',
          type: 'image/png',
          purpose: 'any maskable',
        },
      ],
    },
  },
});

That configuration does heavy lifting: it auto-generates a service worker, precaches your static assets, and creates a valid Web App Manifest. The strategies: 'auto' option intelligently chooses between workbox strategies based on file type.

For Vue standalone projects (without Nuxt), use vite-plugin-pwa instead:

typescript
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { VitePWA } from 'vite-plugin-pwa';

export default defineConfig({
  plugins: [
    vue(),
    VitePWA({
      registerType: 'autoUpdate',
      manifest: { /* ... */ },
      workbox: { /* ... */ },
    }),
  ],
});

Manifest: The App's Identity and Branding

The Web App Manifest is a JSON file that tells the browser how to display your app when installed. It's your app's resume—name, icons, theme colors, and display mode all live here.

Critical manifest fields for production PWAs:

  • name and short_name: Full and abbreviated names for app stores and home screens.
  • icons: Multiple sizes (192×192 and 512×512 minimum) in both standard and maskable formats for adaptive icons.
  • theme_color and background_color: Colors for the address bar and splash screen.
  • display: Usually standalone to hide browser UI and feel like a native app.
  • start_url: Where the app launches when installed from home screen.

A real-world example: a ride-hailing app needs display: 'standalone', a bright theme_color, and maskable icons that work on modern Android devices:

typescript
manifest: {
  name: 'RideHub - Quick Rides',
  short_name: 'RideHub',
  display: 'standalone',
  theme_color: '#FF6B35',
  background_color: '#ffffff',
  scope: '/',
  start_url: '/?mode=pwa',
  icons: [
    {
      src: '/icons/icon-192.png',
      sizes: '192x192',
      type: 'image/png',
      purpose: 'any',
    },
    {
      src: '/icons/icon-192-maskable.png',
      sizes: '192x192',
      type: 'image/png',
      purpose: 'maskable',
    },
    {
      src: '/icons/icon-512.png',
      sizes: '512x512',
      type: 'image/png',
      purpose: 'any',
    },
  ],
}

Generate icons using tools like png-to-ico or design platforms that export multiple sizes. Maskable icons ensure your logo looks good on devices that apply system-defined shapes (circles, rounded squares).

Caching Strategies: Asset and Data Handling

The service worker's caching strategy determines when your app works offline and how fresh data feels. We typically employ two strategies:

Cache-first for static assets: JavaScript, CSS, and images are versioned and rarely change. Serve them from cache, validate in the background:

typescript
pwa: {
  workbox: {
    runtimeCaching: [
      {
        urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i,
        handler: 'CacheFirst',
        options: {
          cacheName: 'google-fonts-cache',
          expiration: {
            maxEntries: 20,
            maxAgeSeconds: 60 * 60 * 24 * 365, // 1 year
          },
        },
      },
    ],
  },
}

Network-first for API data: For REST or GraphQL endpoints, attempt the network first to get fresh data, fall back to cache if offline:

typescript
runtimeCaching: [
  {
    urlPattern: /^https:\/\/api\.example\.com\/.*/i,
    handler: 'NetworkFirst',
    options: {
      cacheName: 'api-cache',
      networkTimeoutSeconds: 3,
      expiration: {
        maxEntries: 50,
        maxAgeSeconds: 60 * 5, // 5 minutes
      },
    },
  },
]

The networkTimeoutSeconds is crucial: if the network takes more than 3 seconds to respond, we abandon the request and use the cache. This prevents your app from appearing frozen on slow connections.

A realistic e-commerce scenario: Product images and bundles are cached long-term (cache-first), but product availability, pricing, and user cart data are network-first with a 5-minute freshness window. This balances speed with data accuracy.

Updates: Prompting Users When New Versions Are Ready

A PWA installed on a user's home screen won't auto-update by default. We need to detect when a new service worker version is available and prompt the user to refresh. The workbox-window library handles this elegantly.

In a Nuxt composable, set up the update listener:

typescript
// composables/usePWAUpdate.ts
import { ref, onMounted } from 'vue';
import { Workbox } from 'workbox-window';

export const usePWAUpdate = () => {
  const updateAvailable = ref(false);
  const wb = ref(null);

  onMounted(() => {
    if (!('serviceWorker' in navigator)) return;

    wb.value = new Workbox('/sw.js');

    wb.value.addEventListener('controlling', () => {
      // New service worker is now active
      window.location.reload();
    });

    wb.value.addEventListener('waiting', () => {
      // New version is waiting; prompt user
      updateAvailable.value = true;
    });

    wb.value.register();
  });

  const promptUpdate = () => {
    if (wb.value?.controller) {
      wb.value.messageSkipWaiting();
    }
  };

  return { updateAvailable, promptUpdate };
};

Then use it in a component to show an update banner:

typescript
// components/PWAUpdatePrompt.vue

  <div>
    <p>A new version of the app is available.</p>
    Update Now
  </div>


When the user clicks "Update Now," the new service worker takes control and reloads the page. On subsequent launches, they get the fresh version.

For a smoother experience in high-traffic apps, consider staggering updates: show the prompt only once per day or batch updates during off-peak hours to avoid overwhelming your CDN.

Common Mistakes

Not using HTTPS in production: Service workers require a secure context. Localhost works for development; production must be HTTPS or the service worker silently fails to register.

Overly aggressive caching: Caching everything forever leaves users stuck on old versions. Use expiration rules and combine cache-first with network-first strategically.

Missing or inadequate icons: Apps without proper icons won't appear in app stores or prompt installation on some devices. Always provide 192×192 and 512×512 versions, plus maskable variants.

Ignoring offline UX: Your app may load offline, but API calls will fail silently. Handle network errors gracefully—show a "You're offline" message, queue actions, or degrade features.

Not testing on real devices: PWA behavior varies by device and OS. An iOS PWA behaves differently than Android. Always test installation and offline mode on target devices.

Key Takeaways

  • Use @vite-pwa/nuxt or vite-plugin-pwa to eliminate PWA boilerplate; they auto-generate service workers and manifests.
  • Manifest configuration defines your app's identity—name, icons (with maskable variants), theme colors, and display mode must be production-ready.
  • Combine cache-first for static assets with network-first for APIs to balance speed and freshness; use networkTimeoutSeconds to prevent hanging on slow networks.
  • Detect and prompt users for updates using workbox-window's waiting event; never force updates without consent.
  • HTTPS is non-negotiable for service worker registration; test on real devices and various OS platforms.
  • Handle offline scenarios gracefully in your UI—users should understand why features aren't working, not silently fail.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles