Vai al contenuto
HexaTransfer
Torna al blog
Approfondimenti tecnici

Caching dei file con Service Worker per trasferimenti offline

Usa i Service Worker per abilitare capacità di trasferimento file offline. Strategie di cache, sincronizzazione in background e pattern per progressive web app.

Il GDPR richiede che i dati personali rimangano protetti anche durante le interruzioni di rete — e i Service Worker permettono a un'app di trasferimento file di continuare a funzionare quando la rete cade: memorizza in cache la shell HTML e il JS con l'API Cache, metti in coda gli upload falliti con l'API Background Sync, salva i blocchi parziali su IndexedDB, e ripeti tutto quando la connettività torna. Il worker opera su un thread separato con il proprio event loop, intercetta gli eventi fetch per il suo scope, e persiste attraverso la chiusura delle schede. Per gli strumenti di upload, la ricetta giusta è: strategia stale-while-revalidate per la shell dell'app, accodamento dei blocchi basato su IndexedDB per i trasferimenti in corso, e una registrazione di Background Sync periodica che riprova gli upload ogni 15 minuti fino al successo.

Cosa fanno davvero i Service Worker per le app di trasferimento

Il vantaggio principale è che navigator.serviceWorker sopravvive ai ricaricamenti delle schede, ai periodi offline e persino al sonno del telefono. Quando un utente avvia un upload da 2 GB su un Wi-Fi traballante del treno, si vuole che i blocchi già completati rimangano completati, quelli in corso vengano ripresi alla riconnessione, e lo stato intero sia recuperabile se il browser chiude la scheda per liberare memoria. Un Service Worker — che opera indipendentemente da qualsiasi scheda specifica — è il componente che abilita tutto ciò.

L'API offre tre mattoni: Cache per memorizzare le risposte per URL, IndexedDB per i dati strutturati (code di blocchi, stato della sessione), e SyncManager per pianificare ripetizioni che si attivano quando il dispositivo è online.

Registrare e versionare il worker

Registra una volta al caricamento dell'app e gestisci esplicitamente gli aggiornamenti:

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js', { scope: '/' })
    .then((reg) => reg.addEventListener('updatefound', () => {
      const sw = reg.installing;
      sw.addEventListener('statechange', () => {
        if (sw.state === 'installed' && navigator.serviceWorker.controller) {
          // nuova versione pronta, chiedi all'utente di aggiornare
        }
      });
    }));
}

Versiona le chiavi della cache (transfer-v7) affinché un nuovo deploy invalidi gli asset vecchi senza lasciare JavaScript obsoleto. Il bug classico: index.html viene memorizzato in cache per sempre, gli utenti non ricevono mai l'aggiornamento, e si passa settimane a fare debug via Slack. Aggancia le cache agli hash di build e pulisci nell'evento activate.

Strategie di caching per la shell dell'app vs. i dati utente

Risorse diverse meritano strategie diverse:

  • Shell dell'app (HTML, CSS, JS, icone): cache-first con fallback di rete. Caricamenti istantanei, funziona offline.
  • Metadati API (/shares/:id): network-first con fallback cache, TTL di 60 secondi. Aggiornato quando online, usabile quando non lo è.
  • Byte del file: non memorizzare mai in cache. I file sono spesso multi-gigabyte e l'API Cache ha quote per origine (tipicamente il 60% dello spazio libero su disco).
  • Font da CDN: stale-while-revalidate. Veloci e auto-aggiornanti.

Nel gestore fetch:

self.addEventListener('fetch', (e) => {
  const url = new URL(e.request.url);
  if (url.pathname.startsWith('/assets/')) {
    e.respondWith(cacheFirst(e.request, 'shell-v7'));
  } else if (url.pathname.startsWith('/api/shares/')) {
    e.respondWith(networkFirst(e.request, 'api-v1', 60));
  }
});

Non intercettare mai le richieste per i caricamenti binari di file — evitali controllando e.request.method === 'PUT' e ritornando subito. Fare proxy di PUT da gigabyte attraverso il worker è un disastro di memoria.

Accodare gli upload falliti con Background Sync

SyncManager è la chiave per gli upload resilienti. Quando un PUT di un blocco fallisce, salvalo in IndexedDB e registra una sincronizzazione:

// nel codice della pagina
const reg = await navigator.serviceWorker.ready;
await reg.sync.register('flush-uploads');
// in sw.js
self.addEventListener('sync', (event) => {
  if (event.tag === 'flush-uploads') {
    event.waitUntil(flushPendingUploads());
  }
});

Il browser attiva l'evento sync quando la rete torna, con backoff esponenziale fino a circa 24 ore. Chrome e Edge lo supportano; Safari ha rilasciato un sottoinsieme nella versione 17.5 sotto il flag "Background Fetch" che richiede il permesso dell'utente. Per Safari, usa il fallback di riprovare alla prossima apertura visibile della pagina tramite visibilitychange.

L'API Background Fetch è uno strumento separato specificamente per le operazioni su file di grandi dimensioni — mostra una notifica persistente nell'interfaccia del browser affinché gli utenti possano tracciare il progresso anche dopo aver chiuso la scheda. Vale la pena usarla per gli upload superiori a 500 MB.

Memorizzare gli upload parziali su IndexedDB

IndexedDB è il tuo spazio di lavoro durevole. Apri un piccolo database una volta, poi salva i metadati della sessione più gli offset dei blocchi:

const db = await openDB('transfers', 1, {
  upgrade(db) {
    db.createObjectStore('sessions', { keyPath: 'id' });
    db.createObjectStore('chunks', { keyPath: ['sessionId', 'index'] });
  }
});
await db.put('sessions', {
  id, fileName, fileSize, fileFingerprint, createdAt: Date.now(),
  completedIndexes: [], partUrls
});

Non memorizzare i byte grezzi dei blocchi — provengono dal File handle, che IndexedDB può persistere come riferimento di clone strutturato che rimane valido attraverso i ricaricamenti. Memorizzare il handle evita di duplicare 2 GB di byte nel database.

Lo storage dell'origine ha dei limiti: circa il 60% dello spazio libero su disco per Chrome desktop, 1 GB per origine su iOS Safari prima che la pressione di evizione entri in gioco. Richiedi navigator.storage.persist() per ottenere il bucket "persistente" che i browser evitano di eliminare automaticamente.

Gestire le transizioni online e offline

Ascolta gli eventi online e offline, sia nella pagina che nel service worker:

// pagina
window.addEventListener('online', () => {
  ui.showBanner('Di nuovo online — ripresa degli upload');
  navigator.serviceWorker.controller?.postMessage({ type: 'resume' });
});
window.addEventListener('offline', () => {
  ui.showBanner('Offline — upload in pausa');
});

navigator.onLine è notoriamente inaffidabile sui captive portal aziendali — riporta true quando il dispositivo ha una connessione di rete locale ma nessuna connessione internet. Per un rilevamento affidabile, esegui un piccolo fetch('/ping', { cache: 'no-store' }) con un timeout di 3 secondi.

Renderla una vera PWA

Distribuisci un manifest.json con display: standalone, un set di icone, e start_url: /. Aggiungi link apple-touch-icon per iOS. Dichiara la gestione dei file affinché il sistema operativo possa associare la tua app a estensioni specifiche:

{
  "name": "Hex Transfer",
  "file_handlers": [{
    "action": "/share-target",
    "accept": { "application/*": [".pdf", ".zip", ".docx"] }
  }]
}

Combinato con Web Share Target, questo consente agli utenti di condividere file dal foglio di condivisione del sistema operativo direttamente nella tua app. Su Chrome Android e Chromium desktop, la PWA può registrarsi come gestore predefinito per i tipi di file che dichiari. Questo trasforma una pagina del browser in un'app che si comporta come uno strumento di upload nativo.

Testare lo scenario offline

Tre scenari da testare manualmente, perché i test offline automatizzati sono instabili:

  1. Avvia un upload da 500 MB su Wi-Fi veloce, passa alla modalità aereo al 30%, aspetta 30 secondi, riattiva il Wi-Fi. L'upload deve riprendere da dove si era fermato senza azione dell'utente.
  2. Avvia un upload, chiudi la scheda al 60%, aspetta 2 minuti, riapri. Offri di riprendere la sessione.
  3. Avvia un upload da mobile, blocca lo schermo per 5 minuti. Il Background Sync deve attivarsi quando lo sblocchi e completare il trasferimento.

La casella di controllo "Offline" di Chrome DevTools e Applicazione > Service Workers > Aggiorna al caricamento sono indispensabili. I profili "Throttling" del pannello Network consentono di simulare Fast 3G e Slow 3G per vedere come si comporta l'interfaccia di errore.

HexaTransfer usa un Service Worker per la cache della shell dell'app e IndexedDB per lo stato delle sessioni in corso, affinché i ricaricamenti e i brevi periodi offline non perdano il progresso dell'upload. Prova su https://hexatransfer.com — gratuito, senza account, massimo 10 GB.

Insidie da conoscere

I Service Worker hanno un piccolo pile di gotcha che mordono i principianti: funzionano solo su HTTPS (tranne localhost), le quote della cache variano enormemente tra i browser, iOS Safari non attiva in modo affidabile i worker per Background Sync, DevTools può memorizzare in cache i worker obsoleti in modo aggressivo (clicca sempre "Bypass per la rete" durante lo sviluppo), e importScripts viene eseguito in modo sincrono durante l'installazione, quindi non recuperare mai script di terze parti lenti lì. Scrivi un piccolo test di integrazione che verifichi che il worker si attivi, rivendichi i client e serva la pagina offline — quel singolo test cattura l'80% delle regressioni che incontrerai in produzione.

Invia file di grandi dimensioni in modo sicuro con crittografia end-to-end

Trasferisci file fino a 10 GB gratuitamente con crittografia end-to-end. Nessun account necessario. I tuoi file vengono crittografati nel browser prima del caricamento — nessun altro può leggerli.

Invia un file