Online-Datei-Uploads mit Chunking und Parallel-Uploads optimieren
Datei-Uploads effizient zu verarbeiten, ist ein entscheidender Aspekt moderner Webanwendungen. Große Dateien können zu langen Upload-Zeiten, Netzwerkunterbrechungen und einer schlechten Nutzererfahrung führen. In diesem Beitrag stellen wir fortgeschrittene Techniken wie Chunking und parallele Uploads vor, mit denen Sie Datei-Uploads optimieren und eine schnellere sowie zuverlässigere Performance erreichen.
Herausforderungen bei Datei-Uploads
Das Hochladen großer Dateien über das Internet bringt mehrere Herausforderungen mit sich. Nutzer erleben möglicherweise langsame Upload-Geschwindigkeiten durch begrenzte Bandbreite oder instabile Netzwerke, und Unterbrechungen erzwingen oft einen Neustart, was zu Frustration führt. Moderne Webanwendungen benötigen robuste Upload-Systeme, die diese Herausforderungen bewältigen und dabei ein nahtloses Nutzererlebnis bewahren.
Chunking erklärt und warum es wichtig ist
Beim Chunking wird eine große Datei in kleinere Teile, sogenannte Chunks, aufgeteilt. Dieser Ansatz bietet mehrere Vorteile:
- Einzelne Chunks lassen sich erneut übertragen, statt die gesamte Datei neu zu starten
- Bessere Speicherverwaltung im Browser
- Einfachere Fortschrittsverfolgung
- Geringere Auswirkungen von Netzwerkunterbrechungen
- Effizientere Fehlerbehebung
Die optimale Chunk-Größe hängt vom Protokoll sowie von Netzwerk- und Server-Limits ab. Dieses Beispiel zielt bei kleinen Dateien auf höchstens zehn Chunks ab und begrenzt jeden Chunk auf 5 MiB. Das ist eine Entscheidung der Anwendung, kein Limit des Browsers. Leere Dateien werden explizit abgelehnt:
const calculateChunkSize = (fileSize) => {
if (!Number.isSafeInteger(fileSize) || fileSize <= 0) {
throw new Error('Select a nonempty file')
}
const MAXIMUM_CHUNK_SIZE = 1024 * 1024 * 5
return Math.min(MAXIMUM_CHUNK_SIZE, Math.ceil(fileSize / 10))
}
Einen einfachen Datei-Upload mit JavaScript einrichten
Platzieren Sie zuerst das HTML und fassen Sie anschließend die folgenden JavaScript-Definitionen in einem Modul zusammen. Das Beispiel setzt Server-Endpunkte voraus, die das im nächsten Abschnitt beschriebene eigene Protokoll implementieren; es ist kein sofort einsetzbarer Client für einen beliebigen Datei-Upload-Endpunkt.
<div id="upload-container">
<label for="file-input">Files to upload</label>
<input type="file" id="file-input" multiple />
<button id="upload-btn">Upload</button>
<button id="cancel-btn">Cancel</button>
<p id="progress" role="status"></p>
</div>
class FileUploader {
constructor() {
this.abortController = null
this.setupEventListeners()
}
setupEventListeners() {
const uploadBtn = document.getElementById('upload-btn')
const cancelBtn = document.getElementById('cancel-btn')
uploadBtn.addEventListener('click', () => this.handleUpload())
cancelBtn.addEventListener('click', () => this.cancelUpload())
}
async handleUpload() {
if (this.abortController) return
const fileInput = document.getElementById('file-input')
const files = fileInput.files
if (files.length === 0) {
document.getElementById('progress').textContent = 'Please select a file.'
return
}
this.abortController = new AbortController()
try {
for (const file of files) {
await this.uploadFile(file)
}
document.getElementById('progress').textContent = 'Upload complete.'
} catch (error) {
document.getElementById('progress').textContent =
error.name === 'AbortError' ? 'Upload canceled.' : 'Upload failed. Please try again.'
} finally {
this.abortController = null
}
}
async uploadFile(file) {
const upload = new SecureUploader(file)
await upload.upload(this.abortController.signal, (percent) => {
this.updateProgress(file, percent)
})
}
cancelUpload() {
if (this.abortController) {
this.abortController.abort()
}
}
updateProgress(file, percentage) {
const progress = document.getElementById('progress')
progress.textContent = `${file.name}: ${Math.round(percentage)}%`
}
}
const uploader = new FileUploader()
Chunk-basierte Uploads implementieren
Der Server muss jede Anfrage authentifizieren, uploadId für den jeweiligen Nutzer autorisieren, Limits für
Größe und Chunk-Anzahl durchsetzen und Chunks idempotent nach (uploadId, chunkNumber) speichern. Beim Abschluss müssen
alle Chunks und ihre Reihenfolge überprüft werden, bevor die Datei veröffentlicht wird. Verwenden
Sie fileName nicht als Dateisystempfad. Lassen Sie unvollständige Uploads auf dem Server verfallen.
Dieses Lehrprotokoll wiederholt Versuche innerhalb einer Seitensitzung; es implementiert weder eine
Wiederherstellung nach einem Neuladen noch ein vollständiges Backend.
Jede der folgenden Anfragen erwartet einen erfolgreichen HTTP-Status; ein JSON-Antwortkörper ist nicht erforderlich.
class ChunkedUploader {
constructor(file, options = {}) {
this.file = file
this.uploadId = crypto.randomUUID()
this.chunkSize = calculateChunkSize(file.size)
this.totalChunks = Math.ceil(file.size / this.chunkSize)
this.retryLimit = options.retryLimit ?? 3
this.retryDelay = options.retryDelay ?? 1000
this.concurrency = options.concurrency ?? 3
if (![this.retryLimit, this.concurrency].every((value) => Number.isInteger(value) && value > 0)
|| !Number.isFinite(this.retryDelay) || this.retryDelay < 0) {
throw new Error('Invalid upload options')
}
}
async uploadChunk(chunk, chunkNumber, signal) {
const formData = new FormData()
formData.append('chunk', chunk)
formData.append('fileName', this.file.name)
formData.append('uploadId', this.uploadId)
formData.append('chunkNumber', chunkNumber)
formData.append('totalChunks', this.totalChunks)
let attempts = 0
while (attempts < this.retryLimit) {
signal.throwIfAborted()
try {
const response = await fetch('/upload-chunk', {
method: 'POST',
body: formData,
signal,
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
return
} catch (error) {
signal.throwIfAborted()
attempts++
if (attempts === this.retryLimit) throw error
await this.waitForRetry(this.retryDelay * 2 ** (attempts - 1), signal)
}
}
}
waitForRetry(milliseconds, signal) {
signal.throwIfAborted()
return new Promise((resolve, reject) => {
const onAbort = () => {
clearTimeout(timer)
reject(signal.reason)
}
const timer = setTimeout(() => {
signal.removeEventListener('abort', onAbort)
resolve()
}, milliseconds)
signal.addEventListener('abort', onAbort, { once: true })
})
}
async upload(signal, onProgress) {
let uploadedBytes = 0
// Upload chunks with a concurrency limit
for (let i = 0; i < this.totalChunks; i += this.concurrency) {
signal.throwIfAborted()
const requests = []
for (let number = i; number < Math.min(i + this.concurrency, this.totalChunks); number++) {
const chunk = this.file.slice(number * this.chunkSize, (number + 1) * this.chunkSize)
requests.push(this.uploadChunk(chunk, number, signal).then(() => {
uploadedBytes += chunk.size
onProgress?.(uploadedBytes / this.file.size * 100)
}))
}
// Settle in-flight requests before reporting failure or allowing another upload.
const results = await Promise.allSettled(requests)
const failure = results.find((result) => result.status === 'rejected')
if (failure) throw failure.reason
}
// Notify server that all chunks are uploaded
const response = await fetch('/complete-upload', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
fileName: this.file.name,
uploadId: this.uploadId,
totalChunks: this.totalChunks,
}),
signal,
})
if (!response.ok) throw new Error(`Finalization failed: HTTP ${response.status}`)
}
}
Parallele Uploads: mehr Geschwindigkeit
Der obige Ansatz nutzt parallele Uploads, indem mehrere Chunks gleichzeitig verarbeitet werden. Diese Technik kann den Durchsatz verbessern, wenn eine einzelne Anfrage die Verbindung nicht auslastet. Messen Sie das mit Ihrem Server und Ihrem Netzwerk: Parallele Anfragen verursachen auch Overhead und können an Rate-Limits stoßen. Der Fortschritts-Callback zählt bestätigte Bytes, nicht die Bytes, die gerade übertragen werden.
Fehler und Retries behandeln
Unsere Implementierung enthält eine robuste Fehlerbehandlung mit automatischen Retries, exponentiellem Backoff und einem sauberen Abbruch mit AbortController. Diese Strategie stellt sicher, dass vorübergehende Netzwerkprobleme oder Serverfehler eine begrenzte Anzahl von Malen erneut versucht werden können. Es empfiehlt sich, klare Fehlermeldungen bereitzustellen und zwischen Netzwerkfehlern und Anwendungsfehlern zu unterscheiden, damit Nutzer Uploads bei Bedarf wiederholen können.
Sicherheit bei Uploads gewährleisten
Die folgende Unterklasse führt grundlegende clientseitige Prüfungen durch, bevor Chunks gesendet werden. Sie dienen nur als frühes Feedback: Ein passender Header beweist nicht, dass eine Datei sicher ist, und Clients können diesen Code umgehen. Der Server muss Inhalt und Größe unabhängig validieren, den Zugriff autorisieren und, wo sinnvoll, Malware-Scans oder Content Disarm & Reconstruction einsetzen.
class SecureUploader extends ChunkedUploader {
async upload(signal, onProgress) {
signal.throwIfAborted()
await this.validateFile()
return super.upload(signal, onProgress)
}
async validateFile() {
// Validate file signature using allowed types
const allowedTypes = ['image/jpeg', 'image/png', 'application/pdf']
if (!allowedTypes.includes(this.file.type)) {
throw new Error('Unsupported file type')
}
const header = new Uint8Array(await this.file.slice(0, 4).arrayBuffer())
if (!this.validateFileSignature(header)) {
throw new Error('Invalid file signature')
}
// Size validation
const maxSize = 100 * 1024 * 1024 // 100 MiB
if (this.file.size > maxSize) {
throw new Error('File too large')
}
}
validateFileSignature(header) {
const signatures = {
'image/jpeg': [0xff, 0xd8, 0xff],
'image/png': [0x89, 0x50, 0x4e, 0x47],
'application/pdf': [0x25, 0x50, 0x44, 0x46],
}
const signature = signatures[this.file.type]
return signature !== undefined && signature.every((byte, i) => header[i] === byte)
}
}
Weitere Sicherheitsmaßnahmen sind:
- Content-Security-Policy-Header (CSP) implementieren.
- Content Disarm & Reconstruction (CDR) und Virenscans einsetzen.
- Eine strikte Validierung von Dateityp und Dateigröße durchsetzen.
Bewährte Verfahren und Optimierungstipps
- Nutzen Sie Web Workers für rechenintensive Aufgaben der Dateiverarbeitung.
- Implementieren Sie, wo sinnvoll, eine clientseitige Dateikomprimierung.
- Speichern Sie für die Wiederherstellung nach einem Neuladen eine vom Server ausgegebene Upload-URL dauerhaft und gleichen Sie deren bestätigten Offset ab; einen Fortschrittsprozentsatz allein zu speichern, reicht nicht aus.
- Überwachen Sie den Speicherverbrauch bei großen Uploads.
- Geben Sie klares visuelles Feedback zum Upload-Status.
- Sorgen Sie für ein sauberes Aufräumen fehlgeschlagener Uploads.
- Nutzen Sie moderne Browser-Funktionen wie Service Workers für Uploads im Hintergrund und ReadableStream für eine effiziente Datenverarbeitung. Die Lebensdauer eines Service Workers ist begrenzt; sie garantiert nicht, dass ein Upload fortgesetzt wird, nachdem der Browser geschlossen wurde.
Fazit: effiziente Upload-Systeme entwickeln
Der Aufbau eines robusten Datei-Upload-Systems erfordert sorgfältige Aufmerksamkeit für Performance,
Sicherheit und Nutzererlebnis. Die besprochenen Techniken bilden eine solide Grundlage für
zuverlässige Datei-Uploads in modernen Webanwendungen. Mit Chunking, parallelen Uploads und modernen
Browser-APIs können Sie effiziente und widerstandsfähige Upload-Systeme entwickeln. Für eine
produktionsreife Lösung mit einer gepflegten Upload-Oberfläche bietet sich Uppy an. Das Folgende ist
eine Alternative zum oben gezeigten eigenen Uploader, keine zusätzliche Chunking-Schicht. XHRUpload
sendet ganze Dateien; verwenden Sie das tus-Plugin von Uppy mit einem kompatiblen tus-Server, wenn
Sie fortsetzbare Uploads benötigen. Installieren Sie @uppy/core, @uppy/dashboard
und @uppy/xhr-upload in Ihrer gebündelten Anwendung und stellen Sie einen autorisierten Endpunkt unter /upload bereit:
import { Uppy } from '@uppy/core'
import Dashboard from '@uppy/dashboard'
import XHRUpload from '@uppy/xhr-upload'
import '@uppy/core/css/style.min.css'
import '@uppy/dashboard/css/style.min.css'
const uppy = new Uppy()
.use(Dashboard, {
inline: true,
target: '#upload-container',
})
.use(XHRUpload, {
endpoint: '/upload',
formData: true,
fieldName: 'file',
})
Diese modernen Ansätze helfen Ihnen zusammen mit sorgfältiger Fehlerbehandlung und Sicherheitsprüfungen dabei, ein nahtloses Datei-Upload-Erlebnis zu schaffen.
