Skip to content
StackPractices
intermediate Por Mathias Paulenko

Progressive Web Apps (PWA) — Referencia Detallada

Referencia Detallada para construir Progressive Web Apps: service workers, soporte offline, Web App Manifest, notificaciones push e instalabilidad.

Temas: frontend

Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.

Visión General

Las Progressive Web Apps (PWA) usan capacidades web modernas para ofrecer una experiencia similar a una app: acceso offline, notificaciones push, instalación en la pantalla de inicio y sincronización en segundo plano. A diferencia de las apps nativas, funcionan en el navegador y no requieren aprobación de una tienda de apps. A continuación: las tecnologías principales y lo que funciona para construir PWAs de producción.

Cuándo Usar

  • For alternatives, see JavaScript Service Worker Offline Caching for PWA.

  • Necesitas funcionalidad offline para una aplicación web

  • Quieres reducir la fricción de las tiendas de apps mientras proporcionas UX similar a nativa

  • Tus usuarios están en móviles con conectividad intermitente

  • Necesitas notificaciones push sin construir apps nativas separadas

  • Quieres mejorar el engagement con prompts de agregar a pantalla de inicio

Tecnologías Principales

TecnologíaPropósitoCapacidad Clave
Service WorkerProxy en segundo plano para solicitudes de redCaché offline, sincronización en segundo plano
Web App ManifestDescribe metadatos de la app para instalaciónIconos, modo de visualización, color del tema
HTTPSRequisito de origen seguro para capacidades PWARequerido para service workers
Push APIMensajes iniciados por el servidorRe-engagement de usuarios incluso cuando la app está cerrada
Background SyncDiferir acciones hasta que la conectividad regreseEncolar envíos de formularios offline

Service Workers

Registro

Registra el service worker al iniciar la app.

// main.js
if ('serviceWorker' in navigator) {
  window.addEventListener('load', async () => {
    try {
      const registration = await navigator.serviceWorker.register('/sw.js');
      console.log('SW registrado:', registration.scope);
    } catch (error) {
      console.error('Fallo en registro de SW:', error);
    }
  });
}

Estrategias de Caché

Elige la estrategia correcta para cada tipo de recurso.

// sw.js — Caché-Primero para assets estáticos
self.addEventListener('fetch', (event) => {
  if (event.request.destination === 'image') {
    event.respondWith(cacheFirst(event.request));
  } else if (event.request.destination === 'document') {
    event.respondWith(networkFirst(event.request));
  }
});

async function cacheFirst(request) {
  const cached = await caches.match(request);
  return cached || fetch(request).then(response => {
    caches.open('v1').then(cache => cache.put(request, response.clone()));
    return response;
  });
}

async function networkFirst(request) {
  try {
    const networkResponse = await fetch(request);
    const cache = await caches.open('v1');
    cache.put(request, networkResponse.clone());
    return networkResponse;
  } catch {
    return caches.match(request);
  }
}

Resumen de Estrategias de Caché

EstrategiaMejor ParaComportamiento
Caché PrimeroAssets estáticos (CSS, JS, imágenes)Servir desde caché; fallback a red
Red PrimeroDocumentos HTML, llamadas APIIntentar red primero; fallback a caché
Stale-While-RevalidateContenido frecuentemente actualizadoServir versión en caché; refrescar en segundo plano
Solo RedDatos en tiempo real (chat, precios)Siempre obtener de la red
Solo CachéApp shell pre-cacheadaNunca consultar la red

Web App Manifest

El manifest habilita agregar a pantalla de inicio y define la experiencia de la app.

{
  "name": "Task Manager Pro",
  "short_name": "Tasks",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#3b82f6",
  "icons": [
    { "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" },
    { "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" }
  ],
  "screenshots": [
    { "src": "/screenshot-1.png", "sizes": "1280x720", "type": "image/png", "form_factor": "wide" },
    { "src": "/screenshot-2.png", "sizes": "750x1334", "type": "image/png", "form_factor": "narrow" }
  ]
}

Experiencia Offline

Patrón de Página Offline

Muestra una página offline personalizada en lugar de la predeterminada del navegador.

// sw.js
const OFFLINE_PAGE = '/offline.html';

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

Sincronización en Segundo Plano

Encola acciones realizadas offline y reintenta cuando la conectividad regresa.

// Encolar una sincronización en segundo plano
async function submitForm(data) {
  try {
    await fetch('/api/submit', { method: 'POST', body: JSON.stringify(data) });
  } catch {
    // Guardar en IndexedDB y registrar para sincronización
    await db.syncQueue.add(data);
    const registration = await navigator.serviceWorker.ready;
    await registration.sync.register('submit-form');
  }
}

// Service Worker maneja el evento de sincronización
self.addEventListener('sync', (event) => {
  if (event.tag === 'submit-form') {
    event.waitUntil(processSyncQueue());
  }
});

Notificaciones Push

Suscribirse a Push

async function subscribeToPush() {
  const registration = await navigator.serviceWorker.ready;
  const subscription = await registration.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY)
  });
  
  // Enviar suscripción al servidor
  await fetch('/api/push-subscribe', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(subscription)
  });
}

Mostrar Notificaciones

// sw.js
self.addEventListener('push', (event) => {
  const data = event.data.json();
  event.waitUntil(
    self.registration.showNotification(data.title, {
      body: data.body,
      icon: '/icon-192.png',
      badge: '/badge-72.png',
      data: { url: data.url },
      actions: [
        { action: 'open', title: 'Abrir App' },
        { action: 'dismiss', title: 'Descartar' }
      ]
    })
  );
});

self.addEventListener('notificationclick', (event) => {
  event.notification.close();
  if (event.action === 'open' || !event.action) {
    event.waitUntil(clients.openWindow(event.notification.data.url));
  }
});

Prompt de Instalación

Prompt a los usuarios para instalar la PWA en navegadores compatibles.

let deferredPrompt;

window.addEventListener('beforeinstallprompt', (e) => {
  e.preventDefault();
  deferredPrompt = e;
  showInstallButton();
});

async function installPWA() {
  if (!deferredPrompt) return;
  deferredPrompt.prompt();
  const { outcome } = await deferredPrompt.userChoice;
  if (outcome === 'accepted') {
    console.log('Usuario instaló la PWA');
  }
  deferredPrompt = null;
}

Pruebas y Depuración

HerramientaPropósito
Chrome DevTools > ApplicationInspeccionar service workers, manifests, cachés
LighthouseAuditar cumplimiento PWA
WebPageTestProbar comportamiento offline en dispositivos reales
ngrokProbar funciones que requieren HTTPS localmente
WorkboxLibrería para simplificar patrones de service worker

Errores Comunes

  • No manejar actualizaciones de caché — los usuarios pueden ver contenido obsoleto indefinidamente
  • Cachear respuestas de API sin versionado — datos obsoletos después de despliegues
  • Ignorar cuotas de almacenamiento — los navegadores pueden eliminar tu caché
  • Sobre-cachear contenido en vivo — usa Red Primero para datos específicos del usuario
  • Faltar HTTPS — service workers y push requieren un origen seguro

Troubleshooting

  • Component does not re-render: verify state reference, props, and memoization. A mutated object can bypass change detection.
  • Style does not apply in production: check that CSS is loaded, class names are not mangled, and specificity wins. Purge unused styles carefully.
  • Build fails after dependency update: read the changelog, pin versions, and clean the lock file. Test in a fresh environment.
  • Accessibility audit fails: add labels, landmarks, focus management, and color contrast. Use a screen reader for manual verification.
  • Hydration mismatch: ensure server and client render the same initial HTML. Avoid using Date, Math.random, or window during SSR.

FAQ

Las PWAs funcionan en iOS? Sí, pero con limitaciones: Safari soporta service workers y agregar a pantalla de inicio, pero las notificaciones push solo están disponibles en iOS 16.4+ para PWAs instaladas.

Cuánto código agrega una PWA? El service worker y el manifest típicamente pesan menos de 5KB. Librerías de terceros como Workbox agregan ~20KB pero manejan casos edge por ti.

Una PWA puede reemplazar mi app nativa? Para apps enfocadas en contenido o moderadamente interactivas, sí. Para juegos, AR/VR pesado o integración profunda con hardware, las apps nativas aún tienen ventajas.

¿Cómo empiezo con esto en un proyecto existente?

Empieza con una parte pequeña y aislada de tu codebase. Aplica los conceptos de esta guía a un módulo o servicio. Mide el impacto, luego expande a otras áreas.

¿Qué herramientas necesito?

Las herramientas mencionadas throughout esta guía se listan en cada sección. La mayoría son open-source y ampliamente adoptadas. Consulta los recursos relacionados para instrucciones de setup.

¿Cómo mido el éxito después de implementar esto?

Define métricas claras antes de empezar: benchmarks de rendimiento, tasas de error o indicadores de mantenibilidad. Compara antes y después. Itera basándote en datos, no en suposiciones.

Errores Comunes en Producción

  • Tratar la guía como un checklist para completar una vez en lugar de una práctica por evolucionar.
  • Adoptar cada recomendación de golpe en lugar de comenzar con un cambio medido.
  • Saltar la evaluación de madurez e imponer prácticas avanzadas a un equipo no preparado.
  • No actualizar runbooks y expectativas de guardia al introducir nuevas prácticas.
  • Ignorar datos reales de incidentes al priorizar qué partes de la guía aplicar primero.
  • No asignar un responsable que revise decisiones trimestralmente.
  • Copiar ejemplos sin adaptarlos a las herramientas y restricciones reales del equipo.
  • Olvidar medir resultados antes de agregar la siguiente mejora.