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

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.
  • Accessibility audit fails: add labels, landmarks, focus management, and color contrast.
  • Hydration mismatch: ensure server and client render the same initial HTML. random, or window during SSR.

Referencia Rápida

  • Comando principal: ejecuta la solución base del artículo y verifica el resultado esperado.
  • Validación: confirma que los tests pasan y que las métricas clave no se degradaron.
  • Rollback: si algo falla, revierte el cambio y consulta la sección de Troubleshooting.

Lectura Adicional

  • Documentación oficial: consulta la referencia actualizada del framework o herramienta utilizada.
  • Guías relacionadas: explora las guías de pwa y service-worker para profundizar.
  • Patrones complementarios: revisa los patrones de diseño aplicables a tu stack tecnológico.
  • Postmortems públicos: estudia incidentes reales de equipos que enfrentaron problemas similares en producción.

Notas de Producción

  • Despliega gradualmente usando canary o blue-green para detectar regresiones temprano.
  • Configura alertas para errores, latencia p99 y tasa de fallos antes de habilitar en producción.
  • Documenta el rollback en el runbook; prueba el procedimiento en staging al menos una vez por trimestre.
  • Revisa logs estructurados con correlation IDs para trazar requests end-to-end en incidentes.

Puntos Clave

  • Aplica progressive web apps (pwa) — referencia detallada cuando necesites una solución práctica para tu caso de uso.
  • Monitorea el rendimiento después de implementar; mide latencia, errores y uso de recursos antes y después.
  • Revisa la sección de Troubleshooting ante errores comunes; la mayoría tienen causa raíz documentada con solución.
  • Mantén dependencias actualizadas y ejecuta tests en CI para prevenir regresiones en producción.

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.

Preguntas frecuentes

¿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.