Ga naar inhoud
HexaTransfer
Terug naar blog
Technische verdiepingen

Progressieve bestandsupload: UX- en technische gids

Bouw progressieve uploadervaringen met drag-and-drop, voortgangsbalken en soepele foutafhandeling voor een betere gebruikerservaring in je app.

Onderzoek van het NCSC toont aan dat gebruikers bij onduidelijke uploadvoortgang vaker een overdracht halverwege afbreken en gevoelige bestanden via onbeveiligde kanalen proberen te sturen. Een progressieve bestandsupload geeft gebruikers op elk moment betrouwbare feedback: direct na het neerzetten van een bestand een bevestiging, tijdens de upload een soepele voortgangsbalk met realistische schatting, bij een fout specifieke herstelopties, en na voltooiing een duidelijke successtatus met vervolgacties. De technische ingrediënten: drag-and-drop via de HTML5 DataTransfer API, chunked uploads met hervatbaarheid, fetch-streaming met ReadableStream voor byte-accurate voortgang, en statusbeheer dat tabbladherladen overleeft via IndexedDB.

Wat "progressief" hier werkelijk betekent

Progressief heeft hier twee betekenissen. Eén: progressive enhancement, zodat de upload werkt als een gewone <input type="file">-POST op een browser uit 2012, en drag-and-drop, chunking en herproberen verkrijgt wanneer JavaScript beschikbaar is. Twee: progressive disclosure, waarbij de UI complexiteit alleen onthult wanneer nodig — toon een percentage tijdens de upload, maar toon herdetails alleen bij een fout. Beide betekenissen wijzen naar hetzelfde principe: de gebruiker mag nooit in een doodlopende weg terechtkomen en mag nooit wachten zonder informatie.

De te vermijden fout is de "spinner des doods" — een generieke laadanimatie die geen indicatie geeft van voortgang, resterende tijd of of er iets fout is gegaan. Gebruikers annuleren uploads die ze niet vertrouwen.

Drag-and-drop die niet vecht met de browser

De HTML5-drag-and-drop-API staat berucht om zijn bugs. Een paar regels die het draaglijk maken:

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]);
});

Roep preventDefault aan op dragover, anders accepteert het droefdoel de drop niet. Gebruik e.dataTransfer.items in plaats van files als u mappen wilt accepteren via webkitGetAsEntry() — dat is de enige manier om mapinhoud recursief op te vangen in Chrome en Firefox.

Maak de fallback ook bruikbaar: een zichtbaar <label> dat een gestylde <input type="file" multiple> omhult, werkt voor 100% van de gebruikers inclusief toetsenbord- en schermlezernavigatie.

Voortgang tonen die gebruikers geloven

Voortgangsbalkjes die springen voelen gebroken. Bereken voortgang als geüploade bytes gedeeld door totale bytes, en smooth dat met een voortschrijdend gemiddelde over twee seconden om jitter te verbergen:

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);
}

Bereken de resterende tijd als (totaleBytes - geüploadeBytes) / throughput(), begrens de weergave tot minimaal vijf seconden en formuleer in menselijke termen: "circa 2 minuten" niet "124,3 seconden." Toon zowel een percentage als een byteteller ("340 MB van 2,1 GB") — gebruikers vergelijken de twee als iets niet klopt.

Foutstatussen met bruikbaar herstel

Generieke "upload mislukt"-meldingen vernietigen vertrouwen. Deel fouten in vijf categorieën in en toon elke categorie anders:

  • Verbindingsuitval (offline-event, TCP-reset): "Opnieuw verbinden..." met automatisch herproberen
  • Server 5xx: "Serverfout, opnieuw proberen over 5s" met handmatige herprobeerknoppe
  • Server 4xx (413 te groot, 415 verkeerd type): "Bestand geweigerd: te groot" met bestandsvervanging
  • Verlopen authenticatie (401, 403): "Sessie verlopen, aanmelden om door te gaan"
  • Clientcrash (JS-fout, browser sloot tabblad): Herstel uit IndexedDB bij herladen

Combineer de melding met de ene actie die het probleem oplost. Als de gebruiker offline is, toon de online/offline-status bewaakt via navigator.onLine en het online-event.

Bytes bijhouden met fetch-streams

XMLHttpRequest.upload.onprogress was de traditionele manier om uploadvoortgang te volgen, maar het is onbetrouwbaar op HTTP/3 en mist bytes gebufferd in de verzendstack. De moderne aanpak gebruikt ReadableStream om bytes te tellen terwijl ze worden geproduceerd:

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);
      }
    }
  });
}

Geef de stream door als de body aan fetch met duplex: 'half'. Safari-ondersteuning voor verzendstreams arriveerde in 17.4; daarvoor valt u terug op XMLHttpRequest. Dit geeft u milliseconde-nauwkeurige voortgang gekoppeld aan werkelijke bytes die naar de netwerkstack zijn gegeven.

Pauzeren, hervatten en annuleren

Gebruikers verwachten een pauzeknop voor alles wat langer dan een minuut duurt. Met chunked uploads is pauzeren gewoon "stop met het uitsturen van nieuwe stukken" en hervatten pakt op waar de werkrij is gebleven. Annuleren gebruikt AbortController:

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

Na het afbreken: verwijder de uploadsessie op de server zodat opslag niet lekt, wis de IndexedDB-vermelding en keer terug naar de beginstatus. Pauzeren moet de status bewaren; annuleren moet die vernietigen. Maak het onderscheid zichtbaar in de UI.

Tabbladherlaad overleven

Bewaar de uploadstatus na elk succesvol stuk:

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

De fingerprint is een SHA-256 van de eerste 1 MB van het bestand plus grootte en lastModified — voldoende om het bestand opnieuw te identificeren wanneer de gebruiker het na herladen opnieuw selecteert. Controleer bij het laden van de pagina op sessies jonger dan een uur in IndexedDB en bied hervatting aan: "U heeft een upload die 12 minuten geleden is gestart. Hervatten?" Hervat niet automatisch zonder toestemming — gebruikers herladen soms de pagina juist om te annuleren.

Toegankelijke en toetsenbordvriendelijke interacties

Een dropzone die alleen op muisbewegingen reageert, faalt voor schermlezergebruikers en toetsenbordgebruikers. Voeg toe:

  • role="button" en tabindex="0" op de dropzone
  • Enter/Spatie-toetsafhandelaar die de bestandsinvoer klikt
  • aria-live="polite" op het voortgangsgebied zodat schermlezers mijlpalen aankondigen
  • Zichtbare focusstijlen, niet alleen hover
  • Duidelijke labels — "Bestand uploaden" slaat beter aan dan "Bladeren" dan een kaal pictogram

Toetsenbordtesten gaat snel: koppel uw muis tien minuten los en probeer een upload te voltooien. Als dat niet lukt, geldt hetzelfde voor een deel van uw gebruikers.

HexaTransfer's uploader gebruikt precies dit progressieve patroon — plain-form-fallback, drag-and-drop-uitbreiding, fetch-streaming, IndexedDB-gebaseerd hervatten en specifiek foutherstel. Probeer het op https://hexatransfer.com — gratis, geen account vereist, maximaal 10 GB.

De details die gebruikers echt opmerken

De afwerking die vergeetbare uploaders onderscheidt van goede, zit in kleine momenten: een dropanimatie die bevestigt dat het bestand is ontvangen, een voortgangsbalk die soepel vult in plaats van te springen, een resterende tijd die nauwkeuriger wordt in plaats van wild te schommelen, specifieke foutmeldingen die vertellen wat u moet doen, een hervattprompt na een per ongeluk herladen pagina, een voltooiingsstatus die lang genoeg aanhoudt om de deel-link te kopiëren, en annuleergedrag dat de upload werkelijk onmiddellijk stopt. Elk van deze is een paar regels code. Implementeer ze allemaal en uw uploader voelt een orde van grootte beter dan de standaard <input type="file">-behandeling.

Verstuur grote bestanden veilig met end-to-end-versleuteling

Draag bestanden tot 10 GB gratis over met end-to-end-versleuteling. Geen account nodig. Uw bestanden worden in uw browser versleuteld voordat ze worden geüpload — niemand anders kan ze lezen.

Een bestand verzenden