Optimización de imágenes para PWA: Rendimiento offline y caché inteligente
Domina la optimización de imágenes en Progressive Web Apps. Estrategias de caché, funcionamiento offline y mejores prácticas para PWAs en 2025.
Las Progressive Web Apps (PWAs) han revolucionado la experiencia móvil, pero su verdadero potencial se desbloquea cuando optimizas correctamente las imágenes para funcionamiento offline y caché inteligente. En 2025, una PWA bien optimizada puede superar en rendimiento a muchas apps nativas.
🚀 Elementos clave para PWAs de alto rendimiento:
- Caché estratégico: Service Workers con políticas Cache-First para imágenes críticas
- Formatos modernos: WebP/AVIF con fallbacks automáticos offline
- Lazy loading avanzado: Intersección con pre-caché inteligente
- Tamaños adaptativos: Responsive images con srcset para cada viewport
- Optimización directa: Comprimir para PWA | Redimensionar responsive
¿Por qué las imágenes son críticas en PWAs?
Las imágenes representan típicamente el 60-80% del peso total de una PWA. A diferencia de las aplicaciones web tradicionales, las PWAs deben funcionar perfectamente offline y ofrecer experiencias nativas, lo que requiere estrategias de optimización específicas:
Desafíos únicos de las PWAs:
- Funcionamiento offline: Imágenes deben estar disponibles sin conexión
- Almacenamiento limitado: Cache Storage API tiene restricciones de tamaño
- Actualizaciones eficientes: Gestión inteligente de versiones de imagen
- Rendimiento móvil: Optimización para dispositivos con recursos limitados
Estrategias de caché para imágenes PWA
1. Cache-First para imágenes críticas
Esta estrategia prioriza la velocidad sirviendo contenido desde caché cuando está disponible:
// Service Worker - Cache-First Strategy
self.addEventListener('fetch', event => {
if (event.request.destination === 'image') {
event.respondWith(
caches.match(event.request)
.then(cachedResponse => {
if (cachedResponse) {
return cachedResponse;
}
return fetch(event.request)
.then(response => {
// Cachear automáticamente nuevas imágenes
const responseClone = response.clone();
caches.open('images-v1')
.then(cache => cache.put(event.request, responseClone));
return response;
});
})
);
}
});
2. Network-First para contenido dinámico
Para imágenes que cambian frecuentemente (avatares, contenido generado por usuarios):
// Estrategia híbrida con timeout
const CACHE_TIMEOUT = 3000;
self.addEventListener('fetch', event => {
if (event.request.url.includes('/dynamic-images/')) {
event.respondWith(
Promise.race([
fetch(event.request),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('timeout')), CACHE_TIMEOUT)
)
])
.then(response => {
// Actualizar caché con nueva versión
const responseClone = response.clone();
caches.open('dynamic-images-v1')
.then(cache => cache.put(event.request, responseClone));
return response;
})
.catch(() => {
// Fallback a caché si network falla
return caches.match(event.request);
})
);
}
});
3. Stale-While-Revalidate para balance óptimo
Sirve desde caché inmediatamente pero actualiza en segundo plano:
// Mejor experiencia de usuario con actualización transparente
self.addEventListener('fetch', event => {
if (event.request.url.includes('/content-images/')) {
event.respondWith(
caches.open('content-images-v1').then(cache => {
return cache.match(event.request).then(cachedResponse => {
const fetchPromise = fetch(event.request).then(networkResponse => {
cache.put(event.request, networkResponse.clone());
return networkResponse;
});
// Devuelve caché inmediatamente si existe, sino espera network
return cachedResponse || fetchPromise;
});
})
);
}
});
Caso de éxito real
Una PWA de e-commerce implementó caché estratégico para 2,000+ imágenes de producto. Resultado: 85% reducción en requests de red, tiempo de carga 2.3x más rápido offline, y 40% mejora en Core Web Vitals Score durante picos de tráfico.
Formatos de imagen optimizados para PWAs
WebP con fallbacks inteligentes
// Detección de soporte y caché diferenciado
const supportsWebP = () => {
const canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = 1;
return canvas.toDataURL('image/webp').indexOf('webp') !== -1;
};
// Service Worker con selección automática de formato
self.addEventListener('fetch', event => {
if (event.request.destination === 'image') {
const url = new URL(event.request.url);
const acceptsWebP = event.request.headers.get('Accept')?.includes('webp');
if (acceptsWebP && !url.pathname.includes('.webp')) {
// Intentar versión WebP primero
const webpUrl = url.pathname.replace(/\.(jpg|jpeg|png)$/i, '.webp');
event.respondWith(
fetch(webpUrl)
.then(response => response.ok ? response : fetch(event.request))
.catch(() => fetch(event.request))
);
}
}
});
AVIF para PWAs premium
Para PWAs que priorizan máxima calidad con mínimo peso:
// Cascada de formatos con caché optimizada
const imageFormats = ['avif', 'webp', 'jpg'];
async function getBestImage(basePath) {
for (const format of imageFormats) {
const url = `${basePath}.${format}`;
const cachedResponse = await caches.match(url);
if (cachedResponse) {
return cachedResponse;
}
try {
const response = await fetch(url);
if (response.ok) {
// Cachear formato exitoso
const cache = await caches.open('optimized-images-v1');
cache.put(url, response.clone());
return response;
}
} catch (error) {
continue; // Intentar siguiente formato
}
}
throw new Error('No image format available');
}
Responsive images para PWAs
srcset optimizado para caché
<picture>
<source
media="(min-width: 800px)"
srcset="hero-1200.webp 1200w, hero-800.webp 800w"
type="image/webp">
<source
media="(min-width: 800px)"
srcset="hero-1200.jpg 1200w, hero-800.jpg 800w">
<source
srcset="hero-480.webp 480w, hero-320.webp 320w"
type="image/webp">
<img
src="hero-480.jpg"
srcset="hero-480.jpg 480w, hero-320.jpg 320w"
sizes="(max-width: 600px) 100vw, 50vw"
alt="Hero image optimizada para PWA"
loading="lazy">
</picture>
Pre-caché selectivo por viewport
// Pre-cargar imágenes críticas según viewport actual
const preloadCriticalImages = () => {
const viewportWidth = window.innerWidth;
const imagesToPreload = [];
if (viewportWidth <= 480) {
imagesToPreload.push(
'/images/hero-320.webp',
'/images/logo-mobile.webp'
);
} else if (viewportWidth <= 800) {
imagesToPreload.push(
'/images/hero-480.webp',
'/images/hero-800.webp'
);
} else {
imagesToPreload.push(
'/images/hero-1200.webp',
'/images/hero-800.webp'
);
}
// Caché proactivo
if ('serviceWorker' in navigator && 'caches' in window) {
caches.open('critical-images-v1').then(cache => {
cache.addAll(imagesToPreload);
});
}
};
// Ejecutar en load y resize
window.addEventListener('load', preloadCriticalImages);
window.addEventListener('resize', debounce(preloadCriticalImages, 300));
Lazy loading avanzado para PWAs
Intersection Observer con pre-caché
// Lazy loading con pre-carga inteligente de siguiente viewport
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
// Cargar imagen actual
if (img.dataset.src) {
img.src = img.dataset.src;
img.onload = () => {
img.classList.remove('loading');
img.classList.add('loaded');
};
}
// Pre-caché siguiente imagen visible
const nextImage = img.closest('.image-container')?.nextElementSibling?.querySelector('img[data-src]');
if (nextImage && 'serviceWorker' in navigator) {
fetch(nextImage.dataset.src).then(response => {
if (response.ok) {
caches.open('preload-images-v1').then(cache => {
cache.put(nextImage.dataset.src, response);
});
}
});
}
observer.unobserve(img);
}
});
}, {
rootMargin: '50px 0px', // Comenzar carga 50px antes de ser visible
threshold: 0.1
});
// Aplicar a todas las imágenes lazy
document.querySelectorAll('img[data-src]').forEach(img => {
imageObserver.observe(img);
});
Placeholder progresivo
/* CSS para transiciones suaves */
.image-container {
position: relative;
overflow: hidden;
background: linear-gradient(45deg, #f0f0f0 25%, transparent 25%),
linear-gradient(-45deg, #f0f0f0 25%, transparent 25%);
background-size: 20px 20px;
background-position: 0 0, 10px 10px;
}
.image-container img {
transition: opacity 0.3s ease;
}
.image-container img.loading {
opacity: 0;
}
.image-container img.loaded {
opacity: 1;
}
/* Blur-up technique para mejor UX */
.image-container::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image: var(--blur-placeholder);
background-size: cover;
filter: blur(5px);
transform: scale(1.1);
transition: opacity 0.3s ease;
}
.image-container.loaded::before {
opacity: 0;
}
Gestión de almacenamiento y límites
Monitoreo de cuota de Storage API
// Verificar espacio disponible y gestionar caché
async function manageStorageQuota() {
if ('storage' in navigator && 'estimate' in navigator.storage) {
const estimate = await navigator.storage.estimate();
const usedSpace = estimate.usage || 0;
const availableSpace = estimate.quota || 0;
const usagePercentage = (usedSpace / availableSpace) * 100;
console.log(`Storage usage: ${usagePercentage.toFixed(1)}%`);
// Limpiar caché si se supera el 80%
if (usagePercentage > 80) {
await cleanOldCache();
}
}
}
async function cleanOldCache() {
const cacheNames = await caches.keys();
const oldCaches = cacheNames.filter(name =>
name.includes('-v') && !name.includes('-v1') // Mantener solo versión actual
);
await Promise.all(
oldCaches.map(cacheName => caches.delete(cacheName))
);
console.log(`Cleaned ${oldCaches.length} old caches`);
}
Estrategia LRU (Least Recently Used)
// Implementar LRU para imágenes cached
class ImageCacheLRU {
constructor(maxSize = 50) {
this.maxSize = maxSize;
this.cache = new Map();
this.usage = new Map();
}
async get(url) {
const cached = await caches.match(url);
if (cached) {
this.usage.set(url, Date.now());
return cached;
}
return null;
}
async put(url, response) {
// Eliminar elementos más antiguos si se supera el límite
if (this.cache.size >= this.maxSize) {
const oldestUrl = [...this.usage.entries()]
.sort((a, b) => a[1] - b[1])[0][0];
const cache = await caches.open('images-lru-v1');
await cache.delete(oldestUrl);
this.cache.delete(oldestUrl);
this.usage.delete(oldestUrl);
}
const cache = await caches.open('images-lru-v1');
await cache.put(url, response);
this.cache.set(url, true);
this.usage.set(url, Date.now());
}
}
const imageLRU = new ImageCacheLRU(100);
Optimiza tu PWA con FotoLince
FotoLince te permite preparar imágenes perfectamente optimizadas para PWAs: formatos modernos, tamaños responsive y compresión inteligente. Todo procesado localmente para integrar directamente en tu estrategia de caché.
Optimizar imágenes para PWA →Métricas y monitoreo de rendimiento
Core Web Vitals específicos para PWAs
// Monitorear LCP específicamente para imágenes
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
if (entry.element && entry.element.tagName === 'IMG') {
console.log('LCP Image:', {
url: entry.element.src,
renderTime: entry.renderTime,
loadTime: entry.loadTime,
size: entry.size
});
// Enviar métricas a analytics
gtag('event', 'lcp_image', {
'custom_parameter': entry.renderTime,
'image_url': entry.element.src
});
}
}
}).observe({entryTypes: ['largest-contentful-paint']});
// Monitorear hit rate de caché
let cacheHits = 0;
let cacheMisses = 0;
const originalFetch = window.fetch;
window.fetch = function(...args) {
const request = args[0];
if (typeof request === 'string' && request.includes('images/')) {
return caches.match(request).then(cachedResponse => {
if (cachedResponse) {
cacheHits++;
return cachedResponse;
} else {
cacheMisses++;
return originalFetch.apply(this, args);
}
});
}
return originalFetch.apply(this, args);
};
// Reportar métricas cada 30 segundos
setInterval(() => {
const hitRate = (cacheHits / (cacheHits + cacheMisses)) * 100;
console.log(`Cache hit rate: ${hitRate.toFixed(1)}%`);
}, 30000);
Análisis de rendimiento offline
// Detectar y optimizar rendimiento offline
window.addEventListener('online', () => {
console.log('Back online - syncing cached images');
// Sincronizar imágenes pendientes
syncPendingImages();
});
window.addEventListener('offline', () => {
console.log('Offline mode - serving from cache');
// Activar modo offline optimizado
enableOfflineOptimizations();
});
function enableOfflineOptimizations() {
// Reducir calidad de imágenes dinámicas
document.documentElement.classList.add('offline-mode');
// Priorizar imágenes críticas en caché
prioritizeCriticalImages();
}
async function prioritizeCriticalImages() {
const criticalImages = document.querySelectorAll('img[data-critical="true"]');
const cache = await caches.open('critical-offline-v1');
for (const img of criticalImages) {
const response = await caches.match(img.src);
if (response) {
await cache.put(img.src, response);
}
}
}
Mejores prácticas de implementación
1. Versionado de caché inteligente
const CACHE_VERSION = 'v2.1.0';
const IMAGE_CACHE_NAME = `images-${CACHE_VERSION}`;
// Auto-limpieza de versiones anteriores
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheName.startsWith('images-') && cacheName !== IMAGE_CACHE_NAME) {
return caches.delete(cacheName);
}
})
);
})
);
});
2. Fallbacks graceful
// Imagen de placeholder cuando todo falla
const FALLBACK_IMAGE = '/images/placeholder.webp';
self.addEventListener('fetch', event => {
if (event.request.destination === 'image') {
event.respondWith(
caches.match(event.request)
.then(response => response || fetch(event.request))
.catch(() => caches.match(FALLBACK_IMAGE))
);
}
});
3. Configuración por tipo de contenido
const CACHE_STRATEGIES = {
'hero-images': { strategy: 'cacheFirst', ttl: 86400000 }, // 24h
'product-images': { strategy: 'staleWhileRevalidate', ttl: 3600000 }, // 1h
'user-avatars': { strategy: 'networkFirst', ttl: 1800000 } // 30min
};
Conclusión
La optimización de imágenes para PWAs va mucho más allá de la simple compresión. Requiere una estrategia integral que combine:
- Caché estratégico adaptado al tipo de contenido
- Formatos modernos con fallbacks automáticos
- Carga inteligente que anticipa las necesidades del usuario
- Gestión eficiente del almacenamiento limitado
- Monitoreo continuo del rendimiento offline y online
Una PWA bien optimizada puede ofrecer experiencias que rivalizan con aplicaciones nativas, especialmente en condiciones de conectividad variable típicas del uso móvil moderno.
¿Listo para crear PWAs de alto rendimiento?
Utiliza FotoLince para optimizar tus imágenes específicamente para PWAs: genera múltiples formatos, tamaños responsive y configuraciones de compresión adaptadas a cada estrategia de caché.
Optimizar imágenes para PWA