Otimização de imagens para PWA: desempenho offline e cache inteligente
Domine a otimização de imagens em Progressive Web Apps. Estratégias de cache, funcionamento offline e melhores práticas para PWAs em 2025.
As Progressive Web Apps (PWAs) revolucionaram a experiência mobile, mas o potencial máximo só aparece quando as imagens são otimizadas para funcionamento offline e cache inteligente. Em 2025, uma PWA bem afinada consegue superar muitas apps nativas em desempenho.
🚀 Elementos-chave para PWAs de alto desempenho:
- Cache estratégico: Service Workers com políticas cache-first para imagens críticas
- Formatos modernos: WebP/AVIF com fallbacks offline automáticos
- Lazy loading avançado: Intersection Observer alinhado a pré-cache inteligente
- Tamanhos adaptativos: imagens responsivas com srcset para cada viewport
- Otimização direta: Comprimir para PWA | Redimensionar responsivo
Por que as imagens são críticas em PWAs?
Imagens respondem por 60-80% do peso total de uma PWA. Diferente de sites tradicionais, PWAs precisam funcionar perfeitamente offline e entregar experiência nativa, exigindo estratégias específicas:
Desafios particulares das PWAs
- Modo offline: imagens devem estar acessíveis sem conexão
- Armazenamento limitado: Cache Storage API impõe restrições de espaço
- Atualizações eficientes: versionamento inteligente das imagens
- Desempenho mobile: otimização para dispositivos com poucos recursos
Estratégias de cache para imagens em PWAs
1. Cache-first para imagens críticas
Garante velocidade máxima servindo o conteúdo diretamente do cache:
// Service Worker - Estratégia Cache-First
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) => {
const responseClone = response.clone();
caches.open('images-v1').then((cache) => cache.put(event.request, responseClone));
return response;
});
})
);
}
});
2. Network-first para conteúdo dinâmico
Ideal para imagens que mudam com frequência (avatares, UGC):
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) => {
const responseClone = response.clone();
caches.open('dynamic-images-v1').then((cache) => cache.put(event.request, responseClone));
return response;
})
.catch(() => caches.match(event.request))
);
}
});
3. Stale-while-revalidate para equilíbrio perfeito
Entrega instantânea a partir do cache enquanto atualiza em segundo plano:
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;
});
return cachedResponse || fetchPromise;
});
})
);
}
});
Caso real de sucesso
Uma PWA de e-commerce aplicou cache estratégico em mais de 2.000 imagens de produto. Resultado: 85% menos requisições, carregamento 2,3x mais rápido offline e melhora de 40% nos Core Web Vitals durante picos de tráfego.
Formatos de imagem ideais para PWAs
WebP com fallbacks inteligentes
const supportsWebP = () => {
const canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = 1;
return canvas.toDataURL('image/webp').includes('webp');
};
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')) {
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
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) {
const cache = await caches.open('optimized-images-v1');
cache.put(url, response.clone());
return response;
}
} catch (error) {
continue;
}
}
throw new Error('Nenhum formato disponível');
}
Imagens responsivas em PWAs
srcset otimizado para cache
<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="Imagem hero otimizada para PWA"
loading="lazy"
>
</picture>
Pré-cache seletivo por viewport
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');
}
if ('serviceWorker' in navigator && 'caches' in window) {
caches.open('critical-images-v1').then((cache) => {
cache.addAll(imagesToPreload);
});
}
};
window.addEventListener('load', preloadCriticalImages);
window.addEventListener('resize', debounce(preloadCriticalImages, 300));
Lazy loading avançado
Intersection Observer com pré-cache
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target;
if (img.dataset.src) {
img.src = img.dataset.src;
img.onload = () => {
img.classList.remove('loading');
img.classList.add('loaded');
};
}
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',
threshold: 0.1,
});
document.querySelectorAll('img[data-src]').forEach((img) => {
imageObserver.observe(img);
});
Placeholder progressivo
.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;
}
.image-container::before {
content: '';
position: absolute;
inset: 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;
}
Gestão de armazenamento e limites
Monitoramento da quota da Storage API
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(`Uso de storage: ${usagePercentage.toFixed(1)}%`);
if (usagePercentage > 80) {
await cleanOldCache();
}
}
}
async function cleanOldCache() {
const cacheNames = await caches.keys();
const oldCaches = cacheNames.filter((name) => name.includes('-v') && !name.includes('-v1'));
await Promise.all(oldCaches.map((cacheName) => caches.delete(cacheName)));
console.log(`Caches removidos: ${oldCaches.length}`);
}
Estratégia LRU (Least Recently Used)
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) {
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);
Otimize sua PWA com FotoLince
O FotoLince prepara imagens perfeitamente ajustadas para PWAs: formatos modernos, tamanhos responsivos e compressão inteligente. Tudo processado localmente para encaixar direto na estratégia de cache.
Otimizar imagens para PWA →Métricas e monitoramento de desempenho
Core Web Vitals focados em PWAs
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,
});
gtag('event', 'lcp_image', {
custom_parameter: entry.renderTime,
image_url: entry.element.src,
});
}
}
}).observe({ entryTypes: ['largest-contentful-paint'] });
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;
}
cacheMisses++;
return originalFetch.apply(this, args);
});
}
return originalFetch.apply(this, args);
};
setInterval(() => {
const hitRate = (cacheHits / (cacheHits + cacheMisses)) * 100;
console.log(`Cache hit rate: ${hitRate.toFixed(1)}%`);
}, 30000);
Análise de desempenho offline
window.addEventListener('online', () => {
console.log('Conexão restabelecida - sincronizando imagens em cache');
syncPendingImages();
});
window.addEventListener('offline', () => {
console.log('Modo offline - servindo do cache');
enableOfflineOptimizations();
});
function enableOfflineOptimizations() {
document.documentElement.classList.add('offline-mode');
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);
}
}
}
Boas práticas de implementação
1. Versionamento de cache inteligente
const CACHE_VERSION = 'v2.1.0';
const IMAGE_CACHE_NAME = `images-${CACHE_VERSION}`;
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 elegantes
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. Configuração por tipo de conteúdo
const CACHE_STRATEGIES = {
'hero-images': { strategy: 'cacheFirst', ttl: 86400000 },
'product-images': { strategy: 'staleWhileRevalidate', ttl: 3600000 },
'user-avatars': { strategy: 'networkFirst', ttl: 1800000 },
};
Conclusão
Otimizar imagens para PWAs vai muito além de comprimir arquivos. É uma estratégia completa que combina:
- Cache inteligente adaptado ao tipo de conteúdo
- Formatos modernos com fallback automático
- Carga antecipada que prevê as necessidades do usuário
- Gestão eficiente do armazenamento limitado
- Monitoramento contínuo do desempenho online e offline
Uma PWA bem otimizada entrega experiências comparáveis (ou superiores) às apps nativas, principalmente em cenários de conectividade instável.
Pronto para lançar PWAs de alta performance?
Use o FotoLince para gerar imagens sob medida para PWAs: formatos múltiplos, tamanhos responsivos e compressão alinhada à sua política de cache.
Otimizar imagens para PWA