Vai al contenuto
HexaTransfer
Torna al blog
Approfondimenti tecnici

Upload progressivo di file: guida tecnica e UX

Costruisci esperienze di upload progressivo con drag-and-drop, barre di progresso e gestione elegante degli errori per una migliore esperienza utente.

Un upload progressivo fornisce all'utente un feedback immediato e affidabile in ogni fase: nel momento in cui rilascia un file, una conferma di selezione; durante l'upload, una barra di progresso fluida con ETA realistica; in caso di errore, opzioni di ripristino specifiche; al completamento, uno stato di successo chiaro con le azioni successive. Gli ingredienti tecnici sono il drag-and-drop tramite l'API HTML5 DataTransfer, gli upload a blocchi con ripristinabilità, lo streaming fetch con ReadableStream per un progresso accurato al byte, e la gestione dello stato che sopravvive ai ricaricamenti della scheda tramite IndexedDB. Fatto bene, un utente che carica un file da 5 GB non si chiede mai se l'app si è bloccata.

Cosa significa davvero "progressivo" qui

Progressivo ha due significati in questo contesto. Uno: progressive enhancement, affinché l'upload funzioni come un semplice POST con <input type="file"> su un browser del 2012, e acquisisca drag-and-drop, chunking e ripetizione dei tentativi quando JavaScript è disponibile. Due: progressive disclosure, dove l'interfaccia rivela la complessità solo quando necessario — mostra una percentuale durante l'upload, ma mostra i dettagli di ripetizione solo in caso di errore. Entrambi i significati puntano allo stesso principio: l'utente non deve mai arrivare a un vicolo cieco, né aspettare senza informazioni.

Il caso di errore da evitare è lo "spinner del destino" — un'animazione di caricamento generica che non dà alcuna indicazione di progresso, ETA o se qualcosa è andato storto. Gli utenti annullano gli upload in cui non hanno fiducia.

Drag-and-drop che non combatte il browser

L'API drag-and-drop HTML5 è famosa per i bug. Alcune regole che la rendono tollerabile:

const dropzone = document.querySelector('.dropzone');
dropzone.addEventListener('dragover', (e) => {
  e.preventDefault();
  dropzone.classList.add('dragging');
});
dropzone.addEventListener('dragleave', () => {
  dropzone.classList.remove('dragging');
});
dropzone.addEventListener('drop', (e) => {
  e.preventDefault();
  dropzone.classList.remove('dragging');
  handleFiles([...e.dataTransfer.files]);
});

Chiama preventDefault su dragover o la zona di rilascio non accetterà il drop. Usa e.dataTransfer.items invece di files se hai bisogno di accettare cartelle tramite webkitGetAsEntry() — è l'unico modo per catturare ricorsivamente il contenuto delle directory su Chrome e Firefox.

Rendi anche il fallback utilizzabile: un <label> visibile che avvolge un <input type="file" multiple> stilizzato funziona per il 100% degli utenti, compresi quelli che usano la tastiera e gli screen reader.

Mostrare un progresso credibile

Le barre di progresso saltano per tre motivi: dimensioni dei blocchi non uniformi, TCP slow-start e buffering nello stack di rete. Levigale con una media mobile di 2 secondi:

const samples = []; // [{ time, bytes }]
function recordSample(bytes) {
  const now = performance.now();
  samples.push({ time: now, bytes });
  while (samples.length > 1 && now - samples[0].time > 2000) samples.shift();
}
function throughput() {
  if (samples.length < 2) return 0;
  const delta = samples[samples.length - 1];
  const base = samples[0];
  return (delta.bytes - base.bytes) / ((delta.time - base.time) / 1000);
}

Calcola l'ETA come (totalBytes - uploadedBytes) / throughput(), imposta il display a un minimo di 5 secondi, e formattalo in termini comprensibili: "circa 2 minuti" non "124,3 secondi". Mostra sia una percentuale che un contatore in byte ("340 MB di 2,1 GB") — gli utenti confrontano i due quando qualcosa sembra strano.

Stati di errore con recupero concreto

I messaggi generici "upload non riuscito" distruggono la fiducia. Classifica i problemi in cinque categorie e visualizza ciascuna in modo distinto:

  • Caduta di rete (evento offline, reset TCP): "Riconnessione in corso..." con ripetizione automatica
  • Errore server 5xx: "Errore del server, nuovo tentativo tra 5s" con pulsante di ripetizione manuale
  • Errore server 4xx (413 troppo grande, 415 tipo errato): "File rifiutato: troppo grande" con sostituzione del file
  • Sessione scaduta (401, 403): "Sessione scaduta, accedi per continuare"
  • Crash client (errore JS, browser ha chiuso la scheda): Recupero da IndexedDB al ricaricamento

Abbina il messaggio all'unica azione che lo risolve. Se l'utente è offline, mostra lo stato online/offline monitorato tramite navigator.onLine e l'evento online.

Tracciare i byte con gli stream Fetch

XMLHttpRequest.upload.onprogress è stato il modo tradizionale di tracciare il progresso degli upload, ma è instabile su HTTP/3 e perde i byte in coda nel buffer di invio. L'approccio moderno usa ReadableStream per contare i byte man mano che vengono prodotti:

function trackedStream(blob, onBytes) {
  let sent = 0;
  return new ReadableStream({
    async pull(controller) {
      const reader = blob.stream().getReader();
      while (true) {
        const { done, value } = await reader.read();
        if (done) { controller.close(); return; }
        sent += value.byteLength;
        onBytes(sent);
        controller.enqueue(value);
      }
    }
  });
}

Passa lo stream come body a fetch con duplex: 'half'. Il supporto Safari per gli stream di richiesta è arrivato nella versione 17.4; prima di quella, torna a XMLHttpRequest. Questo offre un progresso accurato al millisecondo legato ai byte effettivamente consegnati allo stack di rete.

Pausa, ripresa e annullamento

Gli utenti si aspettano un pulsante di pausa su qualsiasi cosa che richieda più di un minuto. Con gli upload a blocchi, la pausa è semplicemente "smetti di inviare nuovi blocchi", e la ripresa riprende dalla coda di lavoro dove si era fermata. L'annullamento usa AbortController:

const ctrl = new AbortController();
cancelButton.onclick = () => ctrl.abort();
await fetch(url, { method: 'PUT', body: blob, signal: ctrl.signal });

Sull'annullamento, pulisci: invia DELETE della sessione di upload sul server affinché lo storage non si sprechi, cancella la voce di IndexedDB, torna allo stato iniziale. La pausa deve preservare lo stato; l'annullamento deve distruggerlo. Rendi visibile la distinzione nell'interfaccia.

Sopravvivere ai ricaricamenti della scheda

Persisti lo stato dell'upload dopo ogni blocco riuscito:

await idb.put('uploads', {
  sessionId, fileFingerprint, fileName, fileSize,
  completedChunks: [...done], updatedAt: Date.now()
}, sessionId);

L'impronta digitale è uno SHA-256 del primo 1 MB del file più dimensione e lastModified — sufficiente per re-identificare il file quando l'utente lo seleziona di nuovo dopo il ricaricamento. Al caricamento della pagina, controlla IndexedDB per sessioni create meno di un'ora fa e offri la ripresa: "Hai un upload in corso da 12 minuti fa. Vuoi riprendere?" Non riprendere automaticamente senza consenso — gli utenti a volte ricaricano proprio per annullare.

Interazioni accessibili e navigabili da tastiera

Una zona di rilascio che risponde solo al trascinamento del mouse fallisce per gli utenti che usano screen reader e tastiera. Aggiungi:

  • role="button" e tabindex="0" sulla zona di rilascio
  • Gestore tasti Invio/Spazio che fa clic sull'input del file
  • aria-live="polite" sulla regione di progresso affinché gli screen reader annuncino le fasi salienti
  • Stili di focus visibili, non solo al passaggio del mouse
  • Etichette chiare — "Carica file" è meglio di "Sfoglia" che è meglio di un'icona senza testo

Il test da tastiera è rapido: scollega il mouse per 10 minuti e prova a completare un upload. Se non riesci, neanche una parte dei tuoi utenti potrà.

HexaTransfer usa esattamente questo schema progressivo — fallback con il form semplice, miglioramento drag-and-drop, streaming fetch, ripresa con IndexedDB, e recupero degli errori specifico. Prova su https://hexatransfer.com — gratuito, senza account, massimo 10 GB.

I dettagli che gli utenti notano davvero

La rifinitura che separa gli uploader memorabili da quelli ottimi vive in piccoli momenti: un'animazione di rilascio che conferma la cattura del file, una barra di progresso che si riempie in modo fluido invece di saltare, un ETA che diventa più preciso nel tempo invece di variare selvaggiamente, messaggi di errore specifici che dicono cosa fare dopo, un prompt di ripresa dopo un aggiornamento accidentale, uno stato di completamento che persiste abbastanza a lungo da copiare il link di condivisione, e un comportamento di annullamento che ferma davvero l'upload immediatamente. Ognuno di questi è poche righe di codice.

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