Progressive Web Apps: Convierte tu Web en App Nativa
17 de diciembre de 2025
Osman Jimenez
PWA Desarrollo Web Mobile
Creando Progressive Web Apps
Las PWAs combinan lo mejor de web y apps nativas: instalables, funcionan offline y son rápidas. Aprende a crear una PWA completa.
Requisitos de una PWA
- HTTPS
- Service Worker
- Web App Manifest
- Responsive Design
Web App Manifest
// manifest.json
{
"name": "Mi Aplicación",
"short_name": "MiApp",
"description": "Una PWA increíble",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#007bff",
"icons": [
{
"src": "/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}Service Worker Básico
// sw.js
const CACHE_NAME = 'my-app-v1';
const urlsToCache = [
'/',
'/styles/main.css',
'/scripts/main.js',
'/images/logo.png'
];
// Instalación
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => cache.addAll(urlsToCache))
);
});
// Fetch con estrategia Cache First
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then((response) => response || fetch(event.request))
);
});
// Actualización
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (cacheName !== CACHE_NAME) {
return caches.delete(cacheName);
}
})
);
})
);
});Registrar Service Worker
// main.js
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then((registration) => {
console.log('SW registered:', registration);
})
.catch((error) => {
console.log('SW registration failed:', error);
});
});
}Estrategias de Caching
1. Cache First (Offline First)
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then((response) => {
return response || fetch(event.request).then((response) => {
return caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, response.clone());
return response;
});
});
})
);
});2. Network First
self.addEventListener('fetch', (event) => {
event.respondWith(
fetch(event.request)
.then((response) => {
const responseClone = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, responseClone);
});
return response;
})
.catch(() => caches.match(event.request))
);
});3. Stale While Revalidate
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;
});
})
);
});PWA en Angular
// Agregar PWA
ng add @angular/pwa
// Esto genera:
// - manifest.webmanifest
// - ngsw-config.json
// - Iconos
// - Service WorkerConfiguración Angular PWA
// ngsw-config.json
{
"index": "/index.html",
"assetGroups": [
{
"name": "app",
"installMode": "prefetch",
"resources": {
"files": [
"/favicon.ico",
"/index.html",
"/*.css",
"/*.js"
]
}
},
{
"name": "assets",
"installMode": "lazy",
"updateMode": "prefetch",
"resources": {
"files": [
"/assets/**",
"/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)"
]
}
}
],
"dataGroups": [
{
"name": "api",
"urls": ["/api/**"],
"cacheConfig": {
"maxSize": 100,
"maxAge": "1h",
"strategy": "freshness"
}
}
]
}Push Notifications
// Solicitar permiso
Notification.requestPermission().then((permission) => {
if (permission === 'granted') {
// Suscribirse a push
navigator.serviceWorker.ready.then((registration) => {
registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(publicKey)
}).then((subscription) => {
// Enviar subscription al servidor
fetch('/api/subscribe', {
method: 'POST',
body: JSON.stringify(subscription),
headers: { 'Content-Type': 'application/json' }
});
});
});
}
});
// En el Service Worker
self.addEventListener('push', (event) => {
const data = event.data.json();
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: '/icons/icon-192x192.png',
badge: '/icons/badge-72x72.png',
data: { url: data.url }
})
);
});
self.addEventListener('notificationclick', (event) => {
event.notification.close();
event.waitUntil(
clients.openWindow(event.notification.data.url)
);
});Detección de Instalación
let deferredPrompt;
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
deferredPrompt = e;
// Mostrar botón de instalación
document.getElementById('install-button').style.display = 'block';
});
document.getElementById('install-button').addEventListener('click', () => {
deferredPrompt.prompt();
deferredPrompt.userChoice.then((choiceResult) => {
if (choiceResult.outcome === 'accepted') {
console.log('Usuario aceptó instalar');
}
deferredPrompt = null;
});
});Testing PWA
- Lighthouse: Auditoría automática
- Chrome DevTools: Application tab
- PWA Builder: Validación completa
Mejores Prácticas
- Iconos múltiples tamaños: 192x192, 512x512
- Splash screens: Para mejor UX
- Offline fallback: Página offline personalizada
- Update strategy: Notifica a usuarios de actualizaciones
- Performance: Optimiza para Lighthouse score > 90
Conclusión
Las PWAs ofrecen una experiencia casi nativa con tecnologías web. Son el futuro del desarrollo móvil para muchos casos de uso, especialmente cuando necesitas alcance multiplataforma sin el overhead de apps nativas.