Chunked File Upload in JavaScript implementieren
Implementieren Sie fortsetzbare Chunked Uploads in JavaScript: Große Dateien handhaben, Fortschritt anzeigen und nach Netzwerkfehlern wiederherstellen.
Chunked-Datei-Uploads in JavaScript teilen eine große Datei in Stücke fester Größe (typischerweise 5–10 MB), laden jeden als separate HTTP-Anfrage hoch und reassemblieren sie serverseitig. Das BSI empfiehlt in seinem IT-Grundschutzkompendium für Webanwendungen, Dateiuploads mit Größenbeschränkungen und Integritätsprüfungen abzusichern — beides ist in einem durchdachten Chunked-Upload-System natürlich enthalten. Das Pattern löst drei reale Probleme: Browser und Proxies brechen Anfragen über 2 GB ab, mobile Netzwerke trennen Verbindungen mittendrin, und Nutzer wollen Fortschrittsrückmeldung. Eine funktionierende Implementierung nutzt File.slice() für Chunks, fetch mit AbortSignal pro Chunk, serverseitige Assemblierung via S3-Multipart oder einem benutzerdefinierten Assembler und einen lokalen Index in IndexedDB, damit die Wiederaufnahme Tab-Neuladen überlebt.
Warum Chunks einfache Uploads schlagen
Eine 4-GB-Datei als eine Anfrage hochzuladen schlägt aus vorhersehbaren Gründen fehl: Nginxs Standard client_max_body_size ist 1 MB, Cloudflare begrenzt kostenlose Tier-Uploads auf 100 MB pro Anfrage, AWS API Gateway stoppt hart bei 10 MB und Mobile Safari beendet Tabs, die einen 4-GB-ArrayBuffer im Speicher halten. Chunked Uploads umgehen jede dieser Grenzen. Man bekommt auch Fortschrittsbalken, die sich tatsächlich bewegen, Wiederholungsversuche, die nicht von null starten, und die Fähigkeit zu pausieren und fortzusetzen. Der Tradeoff: mehr serverseitiger Zustand und mehr Round-Trips — etwa eine HTTP-Anfrage pro 5 MB, was bei einer 10-GB-Datei 2.000 Anfragen bedeutet.
Die richtige Chunk-Größe wählen
Chunk-Größe ist ein Durchsatz-versus-Resilienz-Tradeoff. Zu klein (unter 1 MB) und man verbringt mehr Zeit mit TLS-Handshakes als mit Daten. Zu groß (über 100 MB) und eine getrennte Verbindung verschwendet Minuten des Uploads. Der Sweet Spot für die meisten Netzwerke ist 5–10 MB, was S3s 5-MB-Multipart-Minimum entspricht und gut mit typischen TCP-Fenstergrößen nach Slow-Start ausgerichtet ist.
Das Netzwerk des Nutzers zuerst messen:
const downlink = navigator.connection?.downlink ?? 10;
const chunkSize = downlink > 20 ? 10 * 1024 * 1024 : 5 * 1024 * 1024;
Auf einer 100-Mbit-Verbindung beenden 10-MB-Chunks in etwa einer Sekunde. Auf 4G geben 5-MB-Chunks bessere Wiederherstellung, wenn man in einen Tunnel fährt.
Die Datei aufteilen und hashen
File.slice() gibt ein Blob zurück, das auf dieselben zugrundeliegenden Disk-Bytes verweist ohne Kopieren, sodass das Aufteilen einer 20-GB-Datei nichts kostet:
function* sliceFile(file, chunkSize) {
for (let offset = 0; offset < file.size; offset += chunkSize) {
yield {
index: Math.floor(offset / chunkSize),
blob: file.slice(offset, offset + chunkSize),
start: offset,
end: Math.min(offset + chunkSize, file.size)
};
}
}
Einen SHA-256-Hash jedes Chunks berechnen, bevor man ihn hochlädt, damit der Server die Integrität verifizieren kann:
const buffer = await chunk.blob.arrayBuffer();
const digest = await crypto.subtle.digest('SHA-256', buffer);
const hash = Array.from(new Uint8Array(digest))
.map(b => b.toString(16).padStart(2, '0')).join('');
Für 10 GB Daten fügt das Hashing etwa 20 Sekunden auf einem modernen Laptop hinzu — lohnenswert, um stille Korruption auf flaky Mobilverbindungen zu erkennen.
Mit kontrollierter Parallelität hochladen
Sequenzielle Uploads verschwenden Bandbreite; unbegrenzte Parallelität crasht den Browser. Ein Concurrency-Limit von 3–4 gleichzeitigen Chunks balanciert beides:
async function uploadAll(file, sessionId) {
const queue = [...sliceFile(file, 5 * 1024 * 1024)];
const workers = Array.from({ length: 4 }, async () => {
while (queue.length) {
const chunk = queue.shift();
await uploadChunk(chunk, sessionId);
emitProgress(chunk.index);
}
});
await Promise.all(workers);
}
Jeder uploadChunk-Aufruf ist ein PUT /upload/:sessionId/:index mit dem Blob als Body und dem Hash in einem Header. AbortController pro Chunk nutzen, sodass einzelne Anfragen abgebrochen werden können ohne den gesamten Batch zu töten.
Wiederholungsversuche ohne den Server zu überlasten
Netzwerkfehler benötigen exponentielles Backoff, keine engen Retry-Schleifen. Eine vernünftige Policy: 3 Versuche, Basis-Delay 500 ms, Jitter bis zu 50 Prozent:
async function uploadChunk(chunk, sessionId, attempt = 0) {
try {
const res = await fetch(`/upload/${sessionId}/${chunk.index}`, {
method: 'PUT', body: chunk.blob, headers: { 'X-Hash': chunk.hash }
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
} catch (e) {
if (attempt >= 3) throw e;
const delay = 500 * 2 ** attempt + Math.random() * 250;
await new Promise(r => setTimeout(r, delay));
return uploadChunk(chunk, sessionId, attempt + 1);
}
}
5xx-Antworten als wiederholbar behandeln, 4xx als fatal (außer 408 und 429). Bei 429 den Retry-After-Header beachten statt des lokalen Backkoffs.
Nach einem Tab-Reload wiederaufnehmen
Upload-Zustand nach jedem erfolgreichen Chunk in IndexedDB persistieren:
await db.put('uploads', {
sessionId, fileName: file.name, fileSize: file.size,
completedChunks: [...completedSet], updatedAt: Date.now()
}, sessionId);
Wenn der Nutzer die Seite mit demselben Datei-Picker erneut öffnet, size, lastModified und Name der Datei gegen gespeicherte Sessions vergleichen. Bei Übereinstimmung den Server fragen, welche Chunks er bereits erhalten hat (ein einfaches GET /upload/:sessionId/status, das ein Bitmap zurückgibt, funktioniert), dann nur fehlende hochladen. Das tus-Protokoll (tus.io) formalisiert genau dieses Pattern mit dem Upload-Offset-Header, und die tus-js-client-Bibliothek liefert eine solide Implementierung, wenn man kein eigenes schreiben möchte.
Chunks serverseitig assemblieren
Zwei ernsthafte Optionen: S3-Multipart-Upload, bei dem jeder Chunk eine PartNumber wird und ein finales CompleteMultipartUpload sie zusammenfügt, oder ein benutzerdefinierter Assembler, der jeden Chunk in eine Temp-Datei schreibt und am Ende konkateniert. S3-Multipart ist günstiger bei Scale, da nie Egress während der Assemblierung bezahlt wird und R2 Zero-Egress-Reads bietet. Der benutzerdefinierte Ansatz ist einfacher zu debuggen und ermöglicht das Streamen-Verschlüsseln während der Assemblierung.
Fortschritt, dem Nutzer vertrauen
Fortschrittsbalken, die herumspringen, fühlen sich kaputt an. Fortschritt als hochgeladene Bytes über Gesamtbytes berechnen, nicht als abgeschlossene Chunks, und ihn mit einem gleitenden Durchschnitt über 2 Sekunden glätten, um Jitter zu verstecken. fetch mit einem ReadableStream und einem Transform nutzen, um Bytes zu zählen, da XMLHttpRequest.upload.onprogress nicht immer zuverlässig auf HTTP/3 feuert. Eine ETA anzeigen, indem verbleibende Bytes durch den trailing Durchsatz geteilt werden, aber auf mindestens 5 Sekunden begrenzen, um das berüchtigte „2 Sekunden verbleibend… für 10 Minuten"-Erlebnis zu vermeiden.
HexaTransfer nutzt genau diese Chunked-plus-fortsetzbare Pipeline für seine 10-GB-Uploads, wobei clientseitiges AES-256-GCM zu jedem Chunk hinzugefügt wird, bevor der PUT erfolgt. Ende-zu-Ende-Verschlüsselung ohne Abstriche bei Zuverlässigkeit oder Fortschrittsanzeige.
Jetzt ausprobieren: https://hexatransfer.com — kostenlos, kein Account, bis zu 10 GB.
Große Dateien sicher mit Ende-zu-Ende-Verschlüsselung senden
Übertragen Sie Dateien bis zu 10 GB kostenlos mit Ende-zu-Ende-Verschlüsselung. Kein Konto erforderlich. Ihre Dateien werden in Ihrem Browser verschlüsselt, bevor sie hochgeladen werden — niemand sonst kann sie lesen.
Datei senden