Ir para o conteúdo principal
14 de setembro de 20258 minutos de leituraProgressive Web Apps

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

  1. Modo offline: imagens devem estar acessíveis sem conexão
  2. Armazenamento limitado: Cache Storage API impõe restrições de espaço
  3. Atualizações eficientes: versionamento inteligente das imagens
  4. 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:

  1. Cache inteligente adaptado ao tipo de conteúdo
  2. Formatos modernos com fallback automático
  3. Carga antecipada que prevê as necessidades do usuário
  4. Gestão eficiente do armazenamento limitado
  5. 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

Precisa otimizar suas imagens?

Experimente nossa ferramenta gratuita para comprimir e otimizar imagens com total privacidade. Todo o processamento acontece no seu navegador.

Abrir a ferramenta