Publicaciones

Cómo convertir tu proyecto Nuxt 4 en una PWA

22/01/2025

10 minutos de lectura

Compartir artículo

Convertir una aplicación Nuxt 4 en una Progressive Web App (PWA) te permite ofrecer a tus usuarios una experiencia más rápida, instalable y funcional incluso sin conexión. En este artículo veremos, de manera estructurada y profesional, cómo integrar el módulo adecuado, qué parámetros configurar y cuáles son las mejores prácticas.


1. Módulo recomendado

En Nuxt 4 la opción oficial y mantenida es @vite-pwa/nuxt, un wrapper de vite-plugin-pwa perfectamente integrado con el ecosistema Nuxt.

Instalación

npx nuxi module add @vite-pwa/nuxt

Esto añadirá automáticamente el módulo a tu nuxt.config.ts.


2. Configuración básica

En nuxt.config.ts define la sección pwa con las claves principales:

// nuxt.config.ts
import { defineNuxtConfig } from 'nuxt/config'

export default defineNuxtConfig({
  modules: ['@vite-pwa/nuxt'],
  pwa: {
    registerType: 'autoUpdate',
    manifest: {
      name: 'Mi Aplicación Nuxt 4',
      short_name: 'MiApp',
      description: 'Aplicación Nuxt 4 con soporte PWA',
      start_url: '/',
      scope: '/',
      display: 'standalone',
      background_color: '#ffffff',
      theme_color: '#0ea5e9',
      icons: [
        {
          src: '/pwa-192x192.png',
          sizes: '192x192',
          type: 'image/png',
          purpose: 'any maskable'
        },
        {
          src: '/pwa-512x512.png',
          sizes: '512x512',
          type: 'image/png',
          purpose: 'any maskable'
        }
      ]
    }
  }
})

Campos clave

  • name / short_name → nombre largo y corto que verá el usuario.
  • start_url y scope → definen el punto de inicio y el ámbito de control del Service Worker.
  • display: 'standalone' → hace que la app se muestre como nativa (sin barra de navegador).
  • theme_color y background_color → afectan a la barra superior y a la pantalla de carga.
  • icons → imprescindible incluir variantes con propósito maskable para asegurar compatibilidad con diferentes launchers.

3. Estrategias de Service Worker

El plugin permite varias estrategias de cacheo mediante Workbox:

pwa: {
  // ...
  workbox: {
    cleanupOutdatedCaches: true,
    navigateFallback: '/offline.html',
    globPatterns: ['**/*.{js,css,html,ico,png,svg}'],
    runtimeCaching: [
      {
        urlPattern: /^https:\/\/api\.miapp\.com\/.*$/,
        handler: 'NetworkFirst',
        options: {
          cacheName: 'api-cache',
          expiration: {
            maxEntries: 50,
            maxAgeSeconds: 60 * 60 * 24
          }
        }
      }
    ]
  }
}

Estrategias recomendadas

  • Precaching → para los assets generados en el build.
  • NetworkFirst → para APIs y contenido dinámico (prioriza la red, con fallback a cache).
  • CacheFirst → para imágenes estáticas o fuentes (prioriza velocidad).
  • StaleWhileRevalidate → ideal para recursos que pueden refrescarse en segundo plano.

4. Actualización de la aplicación

La propiedad registerType controla cómo se gestionan las actualizaciones:

  • autoUpdate → el Service Worker se actualiza de manera transparente.
  • prompt → puedes mostrar un aviso al usuario antes de aplicar la nueva versión.

Además, el módulo expone la API $pwa con señales reactivas como needRefresh u offlineReady. Ejemplo de uso en un componente:

<template>
  <div v-if="$pwa?.needRefresh" class="update-toast">
    Nueva versión disponible.
    <button @click="$pwa.updateServiceWorker()">Actualizar</button>
  </div>
</template>

5. Experiencia en iOS

Aunque iOS no soporta todas las capacidades PWA, es recomendable:

  • Definir theme_color y background_color.
  • Añadir apple-touch-icon en app.vue o app.html.
  • Probar en dispositivos reales, ya que iOS maneja la instalación y el modo standalone de forma distinta a Android.

6. Pruebas y validación

  1. Ejecuta un build de producción y prueba con nuxi preview.
  2. Abre Chrome DevTools → Application → Service Workers para verificar el registro.
  3. Usa Lighthouse (Auditoría PWA) para comprobar cumplimiento y detectar mejoras.

7. Página offline

Cuando un usuario pierda conexión y navegue a una página no cacheada, verá este fallback. Este es un ejemplo de offline.html listo para colocar en la carpeta public/ de tu proyecto Nuxt 4 (o en static/, según cómo tengas estructurado).

<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Sin conexión</title>
  <style>
    body {
      margin: 0;
      font-family: system-ui, sans-serif;
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      height: 100vh;
      text-align: center;
      background: #f9fafb;
      color: #111827;
    }
    h1 {
      font-size: 2rem;
      margin-bottom: 1rem;
      color: #0ea5e9;
    }
    p {
      margin-bottom: 2rem;
      color: #374151;
    }
    button {
      background: #0ea5e9;
      color: #fff;
      border: none;
      padding: 0.75rem 1.5rem;
      font-size: 1rem;
      border-radius: 0.5rem;
      cursor: pointer;
    }
    button:hover {
      background: #0284c7;
    }
  </style>
</head>
<body>
  <h1>Estás sin conexión</h1>
  <p>No pudimos cargar el contenido. Vuelve a intentarlo cuando recuperes internet.</p>
  <button onclick="location.reload()">Reintentar</button>
</body>
</html>

👉 Una vez añadido:

  • Si en tu nuxt.config.ts ya tienes configurado:
pwa: {
  workbox: {
    navigateFallback: '/offline.html'
  }
}

8. Ejemplo completo

Vamos a extender la configuración con reglas de runtimeCaching específicas para APIs, imágenes y fuentes, que suelen ser los tres recursos más comunes a optimizar en una PWA.

// nuxt.config.ts
import { defineNuxtConfig } from 'nuxt/config'

export default defineNuxtConfig({
  modules: ['@vite-pwa/nuxt'],
  pwa: {
    registerType: 'autoUpdate',
    manifest: {
      name: 'Mi Aplicación Nuxt 4',
      short_name: 'MiApp',
      description: 'Aplicación Nuxt 4 con soporte PWA',
      start_url: '/',
      scope: '/',
      display: 'standalone',
      background_color: '#ffffff',
      theme_color: '#0ea5e9',
      icons: [
        {
          src: '/pwa-192x192.png',
          sizes: '192x192',
          type: 'image/png',
          purpose: 'any maskable'
        },
        {
          src: '/pwa-512x512.png',
          sizes: '512x512',
          type: 'image/png',
          purpose: 'any maskable'
        }
      ]
    },
    workbox: {
      cleanupOutdatedCaches: true,
      globPatterns: ['**/*.{js,css,html,ico,png,svg}'],
      navigateFallback: '/offline.html',
      runtimeCaching: [
        // 1. API: Network First (prioriza datos frescos)
        {
          urlPattern: /^https:\/\/api\.miapp\.com\/.*$/,
          handler: 'NetworkFirst',
          options: {
            cacheName: 'api-cache',
            networkTimeoutSeconds: 10,
            cacheableResponse: { statuses: [0, 200] },
            expiration: {
              maxEntries: 50,
              maxAgeSeconds: 60 * 60 * 24 // 1 día
            }
          }
        },
        // 2. Imágenes: Cache First (rápidas y raramente cambian)
        {
          urlPattern: /\.(?:png|jpg|jpeg|svg|gif|webp|avif)$/,
          handler: 'CacheFirst',
          options: {
            cacheName: 'image-cache',
            cacheableResponse: { statuses: [0, 200] },
            expiration: {
              maxEntries: 100,
              maxAgeSeconds: 60 * 60 * 24 * 30 // 30 días
            }
          }
        },
        // 3. Fuentes: Cache First (ideal para Google Fonts u otros CDNs)
        {
          urlPattern: /^https:\/\/fonts\.(?:googleapis|gstatic)\.com\/.*/i,
          handler: 'CacheFirst',
          options: {
            cacheName: 'font-cache',
            cacheableResponse: { statuses: [0, 200] },
            expiration: {
              maxEntries: 20,
              maxAgeSeconds: 60 * 60 * 24 * 365 // 1 año
            }
          }
        }
      ]
    }
  }
})

Estrategia de cada recurso:

  • APIs → NetworkFirst: intentará siempre obtener la respuesta más reciente y usará la caché solo si no hay red.
  • Imágenes → CacheFirst: entrega la imagen desde caché si existe, reduciendo tiempo de carga.
  • Fuentes → CacheFirst: útil para Google Fonts, que rara vez cambian.

Con esta configuración tu PWA se vuelve mucho más ágil y confiable en entornos con mala conectividad.

Puntos importantes:

  • Coloca tu offline.html en la carpeta public/ (recomendado).
  • El navigateFallback hará que cualquier navegación sin conexión que no esté en caché redirija a esa página.
  • Puedes extender el runtimeCaching para imágenes, fuentes u otros endpoints externos (ej. un CDN).
  • Con autoUpdate el service worker se actualizará en segundo plano sin intervención del usuario.

8. Buenas prácticas

  • Usa siempre HTTPS en producción.
  • Incluye iconos en varias resoluciones (192px, 512px como mínimo).
  • Limita cachés con maxEntries y maxAgeSeconds para evitar ocupar demasiado almacenamiento.
  • Añade una página /offline.html que proporcione feedback al usuario sin conexión.
  • Coloca tu offline.html en la carpeta public/ (recomendado).
  • El navigateFallback hará que cualquier navegación sin conexión que no esté en caché redirija a esa página.
  • Puedes extender el runtimeCaching para imágenes, fuentes u otros endpoints externos (ej. un CDN).
  • Con autoUpdate el service worker se actualizará en segundo plano sin intervención del usuario.
  • Documenta tu estrategia de actualización (automática o con aviso).

9. Perfecto ✅ Aquí tienes un checklist de pruebas con Lighthouse para validar tu PWA en Nuxt 4. Lo puedes usar como guía rápida después de hacer nuxi build y nuxi preview.


📝 Checklist Lighthouse para PWAs en Nuxt 4

1. Instalabilidad

  • Manifest detectado → Lighthouse debe mostrar que tu manifest.json es válido.
  • Iconos adecuados → Incluye al menos 192×192 y 512×512, con purpose: maskable.
  • start_url válido → Se abre correctamente la app al iniciar.
  • display = standalone → La app se abre como aplicación nativa, sin la barra del navegador.

2. Funcionalidad offline

  • Service Worker activo → Verifica en Chrome DevTools → Application → Service Workers.
  • Página offline → Navega sin conexión a una ruta que no esté en caché y comprueba que aparece offline.html.
  • Precaching correcto → Asegúrate de que los assets _nuxt/ están cacheados en la primera carga.

3. Rendimiento y recursos

  • Runtime caching:
    • API con estrategia NetworkFirst.
    • Imágenes con CacheFirst (máx. 30 días).
    • Fuentes con CacheFirst (máx. 1 año).
  • Limite de caché configurado (maxEntries y maxAgeSeconds).
  • Evitar recursos obsoletoscleanupOutdatedCaches: true habilitado.

4. Accesibilidad y UX

  • Theme color definido → Se refleja en la barra de estado del dispositivo.
  • Background color definido → Pantalla de carga consistente al abrir la app.
  • Meta iOS:
    • apple-touch-icon disponible.
    • mobile-web-app-capable o apple-mobile-web-app-capable configurado si necesitas soporte específico.

5. Actualizaciones

  • registerType configurado:
    • autoUpdate si quieres que todo se actualice en segundo plano.
    • prompt si prefieres notificar al usuario antes de actualizar.
  • Prueba de actualización → Haz un cambio en tu código, vuelve a hacer build y recarga la app: el SW debería actualizarse.

6. Auditoría completa

En Lighthouse (Chrome DevTools → Audits → PWA), revisa:

  • Fast and reliable (rápida y confiable).
  • Installable (instalable en dispositivos).
  • PWA Optimized (usa buenas prácticas).

Consejo extra: prueba tu PWA en modo avión desde el móvil real (Android e iOS). En desktop puede engañar un poco porque el navegador sigue teniendo cachés de desarrollo.


Conclusión

Convertir tu proyecto Nuxt 4 en una PWA con @vite-pwa/nuxt es un proceso directo y altamente configurable. Definir un manifest claro, implementar estrategias de cache adecuadas y cuidar la experiencia de actualización garantizará que tu aplicación no solo sea instalable, sino que además ofrezca un rendimiento superior y una experiencia de usuario consistente.

© 2026, Joaquín J. Abenza - Todos los derechos reservados.
Un simple componente Vue para escuchar la radio