Introducción a las Progressive Web Apps (PWA)

Imagen para Introducción a las Progressive Web Apps (PWA)

¿Te imaginas que tu sitio web pudiera funcionar offline, recibir notificaciones push, instalarse en el dispositivo del usuario como una app nativa, y todo esto sin pasar por ninguna tienda de aplicaciones? Bienvenido al mundo de las Progressive Web Apps (PWA).

Las PWAs representan el futuro del desarrollo web: combinan lo mejor de las aplicaciones web y las apps nativas para ofrecer experiencias rápidas, confiables y atractivas. En este artículo, aprenderás qué son, cómo funcionan y cómo crear tu primera PWA.

¿Qué es una Progressive Web App?

Una PWA es una aplicación web que utiliza capacidades modernas del navegador para ofrecer una experiencia similar a una app nativa. Las características principales incluyen:

  • Instalable: Los usuarios pueden añadirla a su pantalla de inicio
  • Funciona offline: Gracias a Service Workers
  • Responsive: Se adapta a cualquier dispositivo
  • Rápida: Carga instantánea incluso con conexiones lentas
  • Segura: Solo funciona con HTTPS
  • Actualizable: Se actualiza automáticamente en segundo plano
  • Notificaciones push: Puede enviar notificaciones aunque no esté abierta

Componentes esenciales de una PWA

Para convertir tu sitio web en una PWA necesitas tres elementos fundamentales:

  1. Manifest (manifest.json): Define cómo se ve y comporta la app
  2. Service Worker: Habilita funcionalidad offline y caché
  3. HTTPS: Conexión segura obligatoria

Vamos a implementar cada uno paso a paso.

1. El Web App Manifest

El manifest es un archivo JSON que describe tu aplicación. Crea un archivo manifest.json en la raíz de tu proyecto:

{
  "name": "Mi Progressive Web App",
  "short_name": "MiPWA",
  "description": "Una increíble PWA de ejemplo",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#2196F3",
  "orientation": "portrait-primary",
  "icons": [
    {
      "src": "/icons/icon-72x72.png",
      "sizes": "72x72",
      "type": "image/png",
      "purpose": "any"
    },
    {
      "src": "/icons/icon-96x96.png",
      "sizes": "96x96",
      "type": "image/png",
      "purpose": "any"
    },
    {
      "src": "/icons/icon-128x128.png",
      "sizes": "128x128",
      "type": "image/png",
      "purpose": "any"
    },
    {
      "src": "/icons/icon-144x144.png",
      "sizes": "144x144",
      "type": "image/png",
      "purpose": "any"
    },
    {
      "src": "/icons/icon-152x152.png",
      "sizes": "152x152",
      "type": "image/png",
      "purpose": "any"
    },
    {
      "src": "/icons/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "any maskable"
    },
    {
      "src": "/icons/icon-384x384.png",
      "sizes": "384x384",
      "type": "image/png",
      "purpose": "any"
    },
    {
      "src": "/icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "any maskable"
    }
  ]
}

Explicación de las propiedades clave:

  • name: Nombre completo de la app (se muestra en la instalación)
  • short_name: Nombre corto (se muestra debajo del icono)
  • start_url: URL que se abre al iniciar la app
  • display: Cómo se muestra la app
    • standalone: Como app nativa (sin barra del navegador)
    • fullscreen: Pantalla completa
    • minimal-ui: Mínimos controles del navegador
    • browser: Navegador normal
  • theme_color: Color de la barra de estado (Android)
  • background_color: Color de fondo de la splash screen
  • icons: Array de iconos en diferentes tamaños

Vincula el manifest en tu HTML:

<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Mi PWA</title>
  
  <!-- Manifest -->
  <link rel="manifest" href="/manifest.json">
  
  <!-- Theme color para navegadores -->
  <meta name="theme-color" content="#2196F3">
  
  <!-- iOS específico -->
  <meta name="apple-mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
  <meta name="apple-mobile-web-app-title" content="MiPWA">
  <link rel="apple-touch-icon" href="/icons/icon-192x192.png">
</head>
<body>
  <h1>¡Mi Primera PWA!</h1>
  <p>Esta app funciona offline y es instalable.</p>
</body>
</html>

2. Service Worker: El corazón de la PWA

El Service Worker es un script que el navegador ejecuta en segundo plano, separado de la página web. Permite interceptar peticiones de red, cachear recursos y funcionar offline.

Registrar el Service Worker

Crea un archivo sw.js en la raíz de tu proyecto y regístralo en tu HTML:

<script>
  // Verificar si el navegador soporta Service Workers
  if ('serviceWorker' in navigator) {
    window.addEventListener('load', () => {
      navigator.serviceWorker.register('/sw.js')
        .then(registration => {
          console.log('Service Worker registrado:', registration);
        })
        .catch(error => {
          console.error('Error al registrar Service Worker:', error);
        });
    });
  }
</script>

Implementar el Service Worker básico

Aquí está el código completo de sw.js:

const CACHE_NAME = 'mi-pwa-v1';
const urlsToCache = [
  '/',
  '/index.html',
  '/styles.css',
  '/script.js',
  '/icons/icon-192x192.png',
  '/icons/icon-512x512.png'
];

// Evento de instalación - cachear recursos
self.addEventListener('install', event => {
  console.log('Service Worker: Instalando...');
  
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => {
        console.log('Service Worker: Cacheando archivos');
        return cache.addAll(urlsToCache);
      })
      .then(() => self.skipWaiting()) // Activar inmediatamente
  );
});

// Evento de activación - limpiar cachés antiguas
self.addEventListener('activate', event => {
  console.log('Service Worker: Activando...');
  
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames.map(cacheName => {
          if (cacheName !== CACHE_NAME) {
            console.log('Service Worker: Eliminando caché antigua:', cacheName);
            return caches.delete(cacheName);
          }
        })
      );
    }).then(() => self.clients.claim()) // Tomar control inmediato
  );
});

// Interceptar peticiones de red
self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
      .then(response => {
        // Si está en caché, devolverlo
        if (response) {
          return response;
        }
        
        // Si no, hacer petición de red
        return fetch(event.request)
          .then(response => {
            // Verificar si es una respuesta válida
            if (!response || response.status !== 200 || response.type !== 'basic') {
              return response;
            }
            
            // Clonar la respuesta
            const responseToCache = response.clone();
            
            // Añadir al caché
            caches.open(CACHE_NAME)
              .then(cache => {
                cache.put(event.request, responseToCache);
              });
            
            return response;
          });
      })
      .catch(() => {
        // Si falla, mostrar página offline (opcional)
        return caches.match('/offline.html');
      })
  );
});

3. Estrategias de caché

Existen diferentes estrategias para manejar el caché. Aquí las más comunes:

Cache First (Caché primero)

Ideal para recursos estáticos que no cambian:

self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
      .then(response => response || fetch(event.request))
  );
});

Network First (Red primero)

Ideal para contenido dinámico, con fallback a caché:

self.addEventListener('fetch', event => {
  event.respondWith(
    fetch(event.request)
      .catch(() => caches.match(event.request))
  );
});

Stale While Revalidate

Devuelve el caché mientras actualiza en segundo plano

Devuelve el caché mientras actualiza en segundo plano:

self.addEventListener('fetch', event => {
  event.respondWith(
    caches.open(CACHE_NAME).then(cache => {
      return cache.match(event.request).then(response => {
        const fetchPromise = fetch(event.request).then(networkResponse => {
          cache.put(event.request, networkResponse.clone());
          return networkResponse;
        });
        return response || fetchPromise;
      });
    })
  );
});

4. Añadir botón de instalación

Mejora la experiencia permitiendo que el usuario instale la PWA con un botón:

<button id="installBtn" style="display: none;">
  Instalar App
</button>

<script>
  let deferredPrompt;
  const installBtn = document.getElementById('installBtn');

  // Capturar el evento de instalación
  window.addEventListener('beforeinstallprompt', (e) => {
    // Prevenir el prompt automático
    e.preventDefault();
    deferredPrompt = e;
    
    // Mostrar el botón
    installBtn.style.display = 'block';
  });

  // Manejar el click del botón
  installBtn.addEventListener('click', async () => {
    if (!deferredPrompt) return;
    
    // Mostrar el prompt
    deferredPrompt.prompt();
    
    // Esperar la respuesta del usuario
    const { outcome } = await deferredPrompt.userChoice;
    console.log(`Usuario ${outcome === 'accepted' ? 'aceptó' : 'rechazó'} la instalación`);
    
    // Limpiar
    deferredPrompt = null;
    installBtn.style.display = 'none';
  });

  // Detectar si ya está instalada
  window.addEventListener('appinstalled', () => {
    console.log('¡PWA instalada exitosamente!');
    deferredPrompt = null;
  });
</script>

5. Notificaciones Push (opcional)

Para implementar notificaciones push necesitas:

// Solicitar permiso
async function requestNotificationPermission() {
  const permission = await Notification.requestPermission();
  
  if (permission === 'granted') {
    console.log('Permiso de notificaciones concedido');
    
    // Crear una notificación de prueba
    new Notification('¡Hola!', {
      body: 'Esta es una notificación de prueba',
      icon: '/icons/icon-192x192.png',
      badge: '/icons/icon-72x72.png'
    });
  }
}

// En el Service Worker
self.addEventListener('push', event => {
  const data = event.data.json();
  
  const options = {
    body: data.body,
    icon: '/icons/icon-192x192.png',
    badge: '/icons/icon-72x72.png',
    vibrate: [200, 100, 200]
  };
  
  event.waitUntil(
    self.registration.showNotification(data.title, options)
  );
});

6. Testing y debugging

Chrome DevTools

  1. Abre DevTools (F12)
  2. Ve a la pestaña "Application"
  3. Verás secciones para:
    • Manifest: Inspeccionar el manifest.json
    • Service Workers: Ver estado y forzar actualización
    • Cache Storage: Ver el contenido del caché
    • Clear storage: Limpiar todo

Lighthouse

Ejecuta una auditoría PWA:

  1. Abre DevTools
  2. Pestaña "Lighthouse"
  3. Selecciona "Progressive Web App"
  4. Click en "Analyze page load"

7. Checklist de PWA

Manifest configurado con todos los iconos ✅ Service Worker registrado y funcionando ✅ HTTPS activo (obligatorio en producción) ✅ Responsive design en todos los dispositivos ✅ Funciona offline o con conexión lenta ✅ Carga rápida (< 3 segundos) ✅ Instalable con prompt de instalación ✅ Lighthouse score > 90 en PWA

Estructura de archivos recomendada

mi-pwa/
├── index.html
├── manifest.json
├── sw.js
├── styles.css
├── script.js
├── offline.html
└── icons/
    ├── icon-72x72.png
    ├── icon-96x96.png
    ├── icon-128x128.png
    ├── icon-144x144.png
    ├── icon-152x152.png
    ├── icon-192x192.png
    ├── icon-384x384.png
    └── icon-512x512.png

Herramientas útiles

Frameworks y PWA

Muchos frameworks modernos tienen soporte nativo para PWA:

Next.js

npm install next-pwa

Create React App

npx create-react-app my-pwa --template cra-template-pwa

Vue CLI

vue add pwa

Casos de uso reales

Las PWAs son perfectas para:

  • E-commerce: Mejora conversión con instalación y notificaciones
  • Noticias y blogs: Lectura offline de artículos
  • Redes sociales: Experiencia similar a app nativa
  • Productividad: Funcionamiento sin conexión constante
  • Juegos casuales: Instalación sin tiendas de apps

Ventajas de las PWAs

Sin tiendas de aplicaciones: Distribución directa ✅ Actualizaciones instantáneas: Sin aprobación de tiendas ✅ Un solo código: Web, iOS, Android ✅ Menor tamaño: Más ligeras que apps nativas ✅ SEO-friendly: Indexables por buscadores ✅ Menor costo: Un equipo de desarrollo

Conclusión

Las Progressive Web Apps representan una evolución natural del desarrollo web. Con relativamente poco esfuerzo, puedes transformar tu sitio web en una aplicación instalable, rápida y que funciona offline.

Los pasos básicos son:

  1. Crear un manifest.json completo
  2. Implementar un Service Worker con estrategia de caché
  3. Servir tu app sobre HTTPS
  4. Optimizar para dispositivos móviles
  5. Pasar la auditoría Lighthouse

Siguiente paso: Toma tu proyecto web actual y conviértelo en PWA. Empieza con el manifest, añade un Service Worker básico, y verás cómo la experiencia de usuario mejora dramáticamente.

¿Listo para construir tu primera PWA?

¿Necesitas ayuda con este tema?

No dudes en contactarme para una asesoría personalizada. ¡Estoy aquí para ayudarte a llevar tu proyecto al siguiente nivel! 🚀