Aplicaciones web progresivas (PWA)

Una aplicación web progresiva (PWA) es una aplicación web que utiliza capacidades modernas del navegador para ofrecer una experiencia similar a la de una aplicación. Las PWA son confiables, rápidas y atractivas: combinan el alcance de la web con las capacidades de las aplicaciones móviles nativas.
Introducción
Una aplicación web progresiva (PWA) es una aplicación web que utiliza capacidades modernas del navegador para ofrecer una experiencia similar a la de una aplicación. Las PWA son confiables, rápidas y atractivas: combinan el alcance de la web con las capacidades de las aplicaciones móviles nativas.
Las PWA resuelven una tensión fundamental: los usuarios quieren la disponibilidad instantánea de la web (sin instalación, vinculable, siempre actualizada) pero esperan el rendimiento y las características de las aplicaciones nativas (soporte sin conexión, notificaciones automáticas, presencia en la pantalla de inicio). Las PWA ofrecen ambas cosas.
Características principales
El término "aplicación web progresiva" fue acuñado por Alex Russell y Frances Berriman de Google en 2015. Una PWA debe cumplir estas 10 características:
| # | Principio | Por qué es importante |
|---|---|---|
| 1 | Progresivo: funciona para todos | Degradación elegante en navegadores más antiguos |
| 2 | Responsivo: se adapta a cualquier pantalla | Escritorio, tableta, móvil |
| 3 | Independiente de la conectividad: funciona sin conexión | Almacenamiento en caché del trabajador del servicio |
| 4 | Parecido a una aplicación: sensación de aplicación nativa | Arquitectura de Shell, navegación fluida |
| 5 | Fresco: siempre actualizado | Ciclo de vida de actualización del trabajador del servicio |
| 6 | Seguro: servido a través de HTTPS | Previene la manipulación, necesaria para los trabajadores de servicios. |
| 7 | Descubrible: compatible con SEO | Los motores de búsqueda indexan las PWA |
| 8 | Reactivable: notificaciones automáticas | Vuelva a atraer a los usuarios como si fueran aplicaciones nativas |
| 9 | Instalable — Agregar a la pantalla de inicio | Manifiesto de la aplicación web |
| 10 | Enlazable: compartir mediante URL | No se requiere tienda de aplicaciones |
Los tres pilares de la PWA
1. Trabajador de servicio: el motor
Un trabajador de servicio es un archivo JavaScript que se ejecuta en segundo plano, separado de la página web. Actúa como un proxy de red programable, interceptando solicitudes y habilitando funcionalidad fuera de línea, notificaciones automáticas y sincronización en segundo plano.
Ciclo de vida del trabajador de servicio
Installing → Installed → Activating → Activated → Idle → Terminated
│ │
└─── (new version detected) ──────────────┘
└─── Waiting (until all tabs close) ─────┘
// service-worker.js
const CACHE_NAME = 'my-pwa-v2';
const ASSETS_TO_CACHE = [
'/',
'/index.html',
'/styles.css',
'/app.js',
'/offline.html',
'/icons/icon-192.png'
];
// Install event — cache static assets
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => {
console.log('Caching app shell');
return cache.addAll(ASSETS_TO_CACHE);
})
);
// Force new service worker to activate immediately
self.skipWaiting();
});
// Activate event — clean old caches
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames
.filter(name => name !== CACHE_NAME)
.map(name => caches.delete(name))
);
})
);
// Take control of all open pages immediately
clients.claim();
});
// Fetch event — intercept network requests
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(cachedResponse => {
// Return cached response if available
if (cachedResponse) {
return cachedResponse;
}
// Otherwise fetch from network
return fetch(event.request).then(networkResponse => {
// Don't cache non-GET or error responses
if (!event.request.url.includes('/api/')) {
return networkResponse;
}
// Cache API responses for offline use
const responseClone = networkResponse.clone();
caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, responseClone);
});
return networkResponse;
}).catch(() => {
// Network failed — return offline fallback
if (event.request.mode === 'navigate') {
return caches.match('/offline.html');
}
return new Response('Offline', { status: 503 });
});
})
);
});
Estrategias de almacenamiento en caché
| Estrategia | Descripción | Caso de uso |
|---|---|---|
| Caché primero | Verificar caché, recurrir a la red | Activos estáticos (CSS, JS, imágenes) |
| Red primero | Pruebe la red, recurra al caché | Llamadas API, contenido dinámico. |
| Obsoleto mientras se revalida | Devolver caché, actualizar en segundo plano | Feeds de noticias, publicaciones de blogs |
| Solo red | Obtener siempre de la red | Datos sensibles (bancarios) |
| Solo caché | Nunca recuperar de la red | shell de la aplicación |
// Stale-while-revalidate strategy
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request).then(cachedResponse => {
const fetchPromise = fetch(event.request).then(networkResponse => {
caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, networkResponse.clone());
});
return networkResponse;
}).catch(() => cachedResponse);
return cachedResponse || fetchPromise;
})
);
});
2. Manifiesto de la aplicación web: la identidad
El manifiesto es un archivo JSON que controla cómo aparece la PWA cuando se instala:
{
"name": "My Progressive Web App",
"short_name": "MyPWA",
"description": "An amazing PWA that works offline",
"start_url": "/?source=pwa",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#3367D6",
"orientation": "portrait-primary",
"scope": "/",
"icons": [
{
"src": "/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/icons/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
],
"categories": ["productivity", "utilities"],
"lang": "en-US",
"dir": "ltr",
"prefer_related_applications": false
}
Propiedades manifiestas explicadas:
display: standalone: se abre sin la interfaz de usuario del navegador (barra de direcciones, pestañas).display: fullscreen: pantalla completa, sin navegador Chrome.display: minimal-ui: controles mínimos del navegador.scope: "/": qué URL forman parte de la PWA.start_url: la página que se abre cuando se inicia la PWA.- Iconos enmascarables: íconos adaptables que funcionan con enmascaramiento a nivel del sistema operativo (redondeado, ondulado, etc.).
3. HTTPS: la base
Los trabajadores de servicios necesitan un contexto seguro. HTTPS es obligatorio para:
- Registro de trabajadores de servicios.
- Notificaciones push.
- El mensaje de instalación (antes del evento de descarga).
# Redirect HTTP to HTTPS
# Nginx
server {
listen 80;
server_name example.com;
return 301 https://$server_name$request_uri;
}
Creación de una PWA: paso a paso
1. Crea el Shell de la aplicación
El shell de la aplicación es el HTML, CSS y JavaScript mínimo necesarios para representar la interfaz de usuario. Se almacena en caché en la primera carga y sirve como base de la PWA.
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#3367D6">
<link rel="manifest" href="/manifest.json">
<link rel="icon" href="/icons/icon-192.png" sizes="192x192">
<link rel="apple-touch-icon" href="/icons/icon-192.png">
<title>My PWA</title>
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<header id="app-header">My PWA</header>
<main id="app-content"></main>
<footer id="app-footer">© 2026</footer>
<!-- Register service worker -->
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js')
.then(reg => console.log('SW registered:', reg.scope))
.catch(err => console.error('SW registration failed:', err));
});
}
</script>
<script src="/app.js"></script>
</body>
</html>
2. Implementar soporte sin conexión
// app.js — Fetch content with offline support
const CACHE_KEY = 'content-cache';
async function loadContent(url) {
try {
const response = await fetch(url);
const data = await response.json();
// Cache for offline
const cache = await caches.open(CACHE_KEY);
cache.put(url, new Response(JSON.stringify(data)));
renderContent(data);
} catch (error) {
// Offline — load from cache
const cached = await caches.match(url);
if (cached) {
const data = await cached.json();
renderContent(data);
showOfflineIndicator();
} else {
showOfflineMessage();
}
}
}
3. Agregar notificaciones automáticas
// Request permission
async function requestNotificationPermission() {
const permission = await Notification.requestPermission();
if (permission === 'granted') {
// Get push subscription
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(publicKey)
});
// Send subscription to server
await fetch('/api/push/subscribe', {
method: 'POST',
body: JSON.stringify(subscription)
});
}
}
// service-worker.js — Handle push events
self.addEventListener('push', event => {
const data = event.data.json();
const options = {
body: data.body,
icon: '/icons/icon-192.png',
badge: '/icons/badge-72.png',
vibrate: [100, 50, 100],
data: { url: data.url },
actions: [
{ action: 'open', title: 'Open' },
{ action: 'close', title: 'Dismiss' }
]
};
event.waitUntil(
self.registration.showNotification(data.title, options)
);
});
self.addEventListener('notificationclick', event => {
event.notification.close();
if (event.action === 'open' || !event.action) {
clients.openWindow(event.notification.data.url);
}
});
PWA versus aplicación nativa
| Característica | PWA | Aplicación nativa |
|---|---|---|
| Distribución | URL (no se requiere tienda) | Tienda de aplicaciones, Google Play |
| Instalar fricción | Un toque (mensaje del navegador) | Descargar, instalar, permisos |
| Tamaño de archivo | KB a MB | 10 MB a 500 MB+ |
| Actualizaciones | Instantáneo (trabajador de servicio) | A través de la revisión de la tienda de aplicaciones |
| Sin conexión | Sí (trabajador de servicios) | Sí (integrado) |
| Notificaciones push | si | si |
| API de dispositivo | Cámara, GPS, acelerómetro, pago | Acceso completo a todas las API del dispositivo |
| Bluetooth | Web Bluetooth (limitado) | API Bluetooth completa |
| Tareas en segundo plano | Sincronización en segundo plano (limitada) | Ejecución completa en segundo plano |
| Rendimiento | Casi nativo (JavaScript/WebAssembly) | Nativo (código compilado) |
| SEO | Indexable por los motores de búsqueda | No indexable |
| Costo de desarrollo | Base de código única | iOS + Android (o multiplataforma) |
| Retención de usuarios | Más bajo (más fácil de salir) | Superior (instalado = comprometido) |
Cuándo elegir PWA en lugar de nativa
- Aplicaciones centradas en contenido: noticias, blogs, revistas.
- Comercio electrónico: sitios de compras (PWA de Pinterest: 40 % más de tiempo invertido).
- Aplicaciones de utilidad: calculadoras, conversores, clima.
- Aplicaciones de baja participación: aplicaciones que los usuarios visitan con poca frecuencia.
- Mercados emergentes: usuarios con almacenamiento y datos limitados.
- Mejora progresiva: experiencia completa en dispositivos modernos, funcional en dispositivos más antiguos.
Cuando la aplicación nativa es mejor
- Juegos de alto rendimiento: más de 60 FPS, renderizado 3D complejo.
- Integración profunda de hardware: periféricos Bluetooth, NFC, ARKit/ARCore.
- Procesamiento en segundo plano: transmisión de música, seguimiento del estado físico.
- Se necesita una alta participación del usuario: redes sociales, mensajería.
Puntos de referencia de rendimiento
| Métrica | Antes de la PWA | Después de la PWA | Mejora |
|---|---|---|---|
| Primera carga | 5s | 1,2s | 76% más rápido |
| Cargas posteriores | 3s | 0,3 s | 90% más rápido |
| Tasa de rebote | 45% | 20% | 55% de reducción |
| Tasa de conversión | 2% | 3.5% | 75% de aumento |
| Páginas por sesión | 3 | 5 | 66% de aumento |
Resultados del mundo real:
| Empresa | Métrica clave | Mejora |
|---|---|---|
| Twitter (X) Lite | Tweets enviados por sesión | 75% de aumento |
| tiempo invertido | 40% de aumento | |
| Úber | Finalización de la reserva | Suave incluso en 2G |
| Starbucks | Usuarios activos diarios | aumento 2x |
| Alibaba | Tasa de conversión | 76% de aumento |
Pruebas y auditorías de PWA
Auditoría de PWA del faro
# Run Lighthouse from CLI
npx lighthouse https://example.com --view --preset=desktop
# PWA-specific checks:
# - ✅ Registers a service worker
# - ✅ Responds with 200 when offline
# - ✅ Web app manifest exists
# - ✅ Manifest has display: standalone
# - ✅ Served over HTTPS
# - ✅ Redirects HTTP to HTTPS
# - ✅ Configurable start URL
# - ✅ Icons provided (192px and 512px)
Lista de verificación de pruebas manuales
## PWA Pre-Flight Checklist
- [ ] HTTPS enabled
- [ ] Service worker registered and active
- [ ] Pages load offline (test with airplane mode)
- [ ] Manifest configured with correct icons
- [ ] "Add to Home Screen" prompt appears
- [ ] PWA opens in standalone mode with no address bar
- [ ] Splash screen displays correctly
- [ ] Push notifications work
- [ ] All pages are responsive (mobile, tablet, desktop)
- [ ] Performance budget met (Lighthouse score > 90)
- [ ] Cross-browser tested (Chrome, Firefox, Safari, Edge)
- [ ] Accessibility audit passes
Funciones avanzadas de PWA
Sincronización en segundo plano
// Register a sync event
async function scheduleSync() {
const registration = await navigator.serviceWorker.ready;
await registration.sync.register('sync-pending-data');
}
// Service worker handles sync
self.addEventListener('sync', event => {
if (event.tag === 'sync-pending-data') {
event.waitUntil(syncPendingData());
}
});
Sincronización periódica en segundo plano
// Request periodic sync (requires user engagement)
const registration = await navigator.serviceWorker.ready;
await registration.periodicSync.register('update-content', {
minInterval: 24 * 60 * 60 * 1000 // Once per day
});
API para compartir web
// Share content using native share sheet
async function shareContent(title, text, url) {
if (navigator.share) {
await navigator.share({ title, text, url });
} else {
// Fallback to custom share UI
copyToClipboard(url);
}
}
API de insignias
// Set app icon badge
if (navigator.setAppBadge) {
await navigator.setAppBadge(5); // Show "5" on icon
} else if (navigator.clearAppBadge) {
await navigator.clearAppBadge();
}
Errores comunes
| trampa | problema | Solución |
|---|---|---|
| Sobrealmacenamiento en caché | La aplicación ofrece contenido obsoleto | Cachés de versión, implementar caché primero para shell, red primero para contenido |
| Sin respaldo sin conexión | Pantalla blanca sin conexión | Siempre almacene en caché /offline.html con un mensaje significativo |
| Errores en la destrucción de caché | El viejo trabajador de servicios persiste | Utilice skipWaiting() y client.claim() |
| Problemas con la pantalla de presentación | Icono o color de tema incorrecto | Configure el manifiesto correctamente, use íconos enmascarables |
| Compatibilidad con safaris | Algunas funciones son limitadas (push, sincronización periódica) | Mejora progresiva: degradar con gracia |
| Tamaño de caché grande | Se superó la cuota de almacenamiento (50 MB por origen) | Eliminar cachés antiguos y limitar el tamaño de la caché |
| Falta la URL de inicio | La pantalla de inicio abre una página incorrecta | Especifique siempre start_url en el manifiesto |
Conclusión
Las aplicaciones web progresivas representan lo mejor de ambos mundos: el alcance y la accesibilidad de la web combinados con las capacidades y la experiencia del usuario de las aplicaciones nativas.
Cuándo construir una PWA
- Quieres el máximo alcance: una aplicación para todas las plataformas.
- El usuario necesita acceso sin conexión: almacenamiento en caché del trabajador del servicio.
- Quieres carga instantánea: arquitectura de shell de la aplicación.
- Quieres volver a interactuar: notificaciones automáticas sin tienda de aplicaciones.
- Tienes una aplicación basada en contenido: noticias, comercio electrónico, servicios públicos.
Empezando
- Agregue un manifest.json e íconos.
- Cree un trabajador de servicio con respaldo sin conexión.
- Ejecute una auditoría Lighthouse y solucione problemas críticos.
- Pruebe en dispositivos reales (Android, iOS, escritorio).
- Implementar y medir la participación de los usuarios.
Las PWA no reemplazan a las aplicaciones nativas en todos los escenarios, pero para la mayoría de las aplicaciones de utilidad y basadas en contenido, brindan una experiencia de usuario superior con una fracción del costo de desarrollo.
Artículos destacados

6 patrones de programación que aprendí de ingenieros senior

Crea un asistente privado de notas con IA
Convierte notas Markdown en un asistente consultable sin exponer borradores privados.

Automatiza la extracción de datos de facturas con OCR
Crea un flujo que lee facturas, extrae campos y marca valores dudosos para revisión.
Comentarios
0 comentariosTodavía no hay comentarios aprobados. Las respuestas nuevas pueden esperar moderación.