Datei-Uploads in Webanwendungen optimieren
Die Upload-Leistung umfasst die Zeit für Dateivorbereitung, Übertragung, Wiederholungen verlorener Anfragen und das Warten auf die Servervalidierung. Dieser DevTip erstellt einen Worker zur Bildvorbereitung mit festen Grenzen und eine Browserintegration für fortsetzbare Uploads, mit Uppy als alternativer Oberfläche. Messen Sie diese Phasen auf Ihren eigenen Geräten und in Ihrem Netzwerk, bevor Sie Kompressionseinstellungen wählen oder die Anzahl paralleler Vorgänge erhöhen.
Häufige Herausforderungen bei Datei-Uploads
Große Dateien können den Arbeitsspeicher erschöpfen, langsame Netzwerke zu Zeitüberschreitungen führen. Bei Unterbrechungen kann im Browser unklar bleiben, ob eine Anfrage erfolgreich war. Eine hilfreiche Oberfläche unterscheidet zwischen übertragenen Bytes und der Annahme durch den Server. Sicherheit beruht auf authentifizierter Eigentumszuordnung und serverseitiger Validierung, auch wenn der Browser offensichtlich ungeeignete Dateien bereits vorab ablehnt.
Bewährte Verfahren zur Optimierung der Upload-Leistung
1. Uploads in Blöcken übertragen
Verwenden Sie ein fortsetzbares Protokoll mit expliziten Offsets, statt anonyme Dateiausschnitte zu senden. Die folgende tus-Integration nutzt Blöcke von 1 MiB und eine begrenzte Anzahl von Retries. Das begrenzt die erneut zu übertragende Datenmenge, verspricht aber keinen höheren Durchsatz für jede Verbindung. HTTP-Fehler müssen zum Fehlschlagen des Vorgangs führen, auch wenn sie erst auftreten, nachdem das letzte Byte den Server erreicht hat.
2. Clientseitige Kompression implementieren
Die Größenänderung von Bildern kann die Datenmenge verringern, beansprucht aber CPU-Ressourcen und kann Qualität und Metadaten beeinträchtigen. Der optionale Worker unten akzeptiert JPEG- oder PNG-Dateien bis 10 MiB, begrenzt decodierte Bilder auf 16 Millionen Pixel, passt beide Abmessungen in 1024 × 1024 ein und erzeugt ein JPEG. Transparenz wird weiß dargestellt; Metadaten bleiben nicht erhalten. Behalten Sie das Original, wenn diese Änderungen nicht akzeptabel sind. Beim Decodieren kann bereits Speicher zugewiesen werden, bevor die Abmessungen bekannt sind. Diese Hilfsfunktion bietet feste Grenzen für normale Nutzerfotos, aber keinen Schutz vor bösartigen Bilddecodern.
Erstellen Sie image-worker.js:
self.onmessage = async ({ data: file }) => {
let bitmap
try {
if (
!(file instanceof Blob) ||
file.size === 0 ||
file.size > 10 * 1024 * 1024 ||
!['image/jpeg', 'image/png'].includes(file.type)
) {
throw new Error('Unsupported image')
}
bitmap = await createImageBitmap(file)
if (bitmap.width * bitmap.height > 16_000_000) throw new Error('Image too large')
const scale = Math.min(1, 1024 / bitmap.width, 1024 / bitmap.height)
const width = Math.max(1, Math.round(bitmap.width * scale))
const height = Math.max(1, Math.round(bitmap.height * scale))
const canvas = new OffscreenCanvas(width, height)
const context = canvas.getContext('2d')
if (!context) throw new Error('Canvas unavailable')
context.fillStyle = 'white'
context.fillRect(0, 0, width, height)
context.drawImage(bitmap, 0, 0, width, height)
const blob = await canvas.convertToBlob({ type: 'image/jpeg', quality: 0.8 })
if (blob.type !== 'image/jpeg' || blob.size === 0) throw new Error('Encoding failed')
self.postMessage({ blob })
} catch {
self.postMessage({ error: 'Image preparation failed.' })
} finally {
bitmap?.close()
}
}
3. Web Workers zur Hintergrundverarbeitung nutzen
Worker sind hier zum Decodieren und Codieren sinnvoll. Fetch und das Aufteilen von Blobs benötigen
keinen eigenen Worker pro Anfrage. Der Wrapper prepare-image.js beendet seinen einzigen
Worker bei Erfolg, Laufzeitfehlern, Fehlern beim Decodieren von Nachrichten, Abbruch,
Zeitüberschreitung oder einem fehlgeschlagenen Aufruf von postMessage:
export function prepareImage(file, signal) {
signal.throwIfAborted()
return new Promise((resolve, reject) => {
const worker = new Worker(new URL('./image-worker.js', import.meta.url), { type: 'module' })
let timer
const finish = (error, blob) => {
clearTimeout(timer)
signal.removeEventListener('abort', abort)
worker.terminate()
if (error) reject(error)
else resolve(new File([blob], 'upload.jpg', { type: 'image/jpeg' }))
}
const abort = () => finish(new Error('Image preparation canceled.'))
signal.addEventListener('abort', abort, { once: true })
timer = setTimeout(() => finish(new Error('Image preparation timed out.')), 30_000)
worker.onerror = () => finish(new Error('Image worker failed.'))
worker.onmessageerror = () => finish(new Error('Invalid worker message.'))
worker.onmessage = ({ data }) => {
if (
!(data?.blob instanceof Blob) ||
data.blob.size === 0 ||
data.blob.type !== 'image/jpeg'
) {
finish(new Error('Image preparation failed.'))
return
}
finish(null, data.blob)
}
try {
worker.postMessage(file)
} catch {
finish(new Error('Could not start image preparation.'))
}
})
}
Weitere Informationen finden Sie in der Plattformdokumentation zu OffscreenCanvas.convertToBlob und Worker.terminate.
Sicherheit bei Datei-Uploads gewährleisten
1. Dateitypen und Dateigrößen validieren
Die Upload-Funktionen akzeptieren Dateien bis 10 MiB. MIME-Typen, Namen und Größen vom Client sind nicht vertrauenswürdige Metadaten. Der Server muss die Datenmenge unabhängig begrenzen, unterstützte Inhalte erkennen und Scan- oder Decodierrichtlinien anwenden, bevor er einen Upload verfügbar macht.
2. Sichere Dateispeicherung verwenden
Die folgenden Browserbeispiele setzen eine bestehende Anwendung mit Authentifizierung und gleichem
Ursprung sowie einen tus-Endpunkt unter /api/tus/files/ voraus. Diese Anleitung behandelt
den Client; sie stellt diesen Server nicht bereit. Konfigurieren Sie das Gateway so, dass es bei
Änderungen die Sitzung und X-CSRF-TOKEN validiert und bei jeder tus-Anfrage
X-Upload-Owner anhand der Sitzung durchsetzt. Erzwingen Sie eine Gesamtgrenze von 10 MiB,
Blöcke von 1 MiB, Speicher- und Anfrageratenlimits pro Nutzer, Ablaufzeiten und eine private
Zwischenspeicherung. Prüfen Sie die Eigentumszuordnung beim Erstellen sowie bei HEAD-, PATCH- und
allen Beendigungsanfragen. Lehnen Sie Weiterleitungen ab und geben Sie ausschließlich Location-URLs
mit gleichem Ursprung unterhalb von /api/tus/files/ aus.
Nachdem tus den Abschluss gemeldet hat, akzeptiert POST /api/upload-publications den Wert
{uploadUrl}. Der Server darf diese URL nur anhand seiner eigenen Upload-Datensätze
mit zugeordnetem Eigentümer auflösen und niemals eine beliebige URL abrufen. Er prüft Vollständigkeit
und Inhalt und veröffentlicht anschließend atomar genau einmal. Doppelte Anfragen liefern dieselbe
Empfangsbestätigung mit HTTP 200 oder 201 zurück. Ein Fehlerstatus oder 202 bestätigt keine
Veröffentlichung. Geben Sie {id} mit einer opaken, nicht leeren Bestätigungs-ID
zurück. Der anfragende Nutzer muss für den Download-Zugriff gesondert autorisiert werden. Zufällige
Dateinamen allein bieten keine Zugriffskontrolle.
Fortsetzbare Uploads mit tus implementieren
Das Protokoll heißt tus. Installieren Sie die getestete Clientversion in Ihrem Browserprojekt:
corepack yarn add --exact tus-js-client@4.3.1
corepack yarn add --dev --exact esbuild@0.27.3
Erstellen Sie upload.js. Übergeben Sie die aktuelle Eigentümer-ID und das CSRF-Token
von Ihrer authentifizierten Seite; keines davon ist ein Transloadit Auth Secret. Diese Funktion
wiederholt unterbrochene Übertragungen innerhalb einer Seitensitzung. Sie ordnet eine neu ausgewählte
Datei nicht automatisch allein anhand ihres Namens einem früheren Upload zu. Ein abgebrochener oder
fehlgeschlagener Upload läuft auf dem Server ab; ein neuer Aufruf erstellt einen neuen Upload.
import { Upload } from 'tus-js-client'
export async function uploadFile(file, { owner, csrf, signal, onProgress }) {
signal.throwIfAborted()
if (
!(file instanceof Blob) ||
file.size === 0 ||
file.size > 10 * 1024 * 1024 ||
!owner ||
!csrf
) {
throw new Error('Choose a file between 1 byte and 10 MiB while signed in.')
}
const headers = { 'X-CSRF-TOKEN': csrf, 'X-Upload-Owner': owner }
const uploadUrl = await new Promise((resolve, reject) => {
let settled = false
let timer
const finish = (error, url) => {
if (settled) return
settled = true
clearTimeout(timer)
signal.removeEventListener('abort', abort)
if (error) {
void upload.abort().catch(() => {})
reject(error)
} else resolve(url)
}
const abort = () => finish(new Error('Upload canceled.'))
const upload = new Upload(file, {
endpoint: '/api/tus/files/',
headers,
chunkSize: 1024 * 1024,
storeFingerprintForResuming: false,
retryDelays: [0, 1000, 3000],
onShouldRetry(error) {
const status = error.originalResponse?.getStatus() ?? 0
return status === 0 || status === 409 || status === 423 || status === 429 || status >= 500
},
onBeforeRequest(request) {
const url = new URL(request.getURL(), location.href)
if (url.origin !== location.origin || !url.pathname.startsWith('/api/tus/files/')) {
throw new Error('Unexpected upload URL')
}
},
onProgress(loaded, total) {
try {
onProgress(Math.min(99, Math.floor((100 * loaded) / total)))
} catch {
finish(new Error('Progress display failed.'))
}
},
onError() {
finish(new Error('Upload failed.'))
},
onSuccess() {
if (!upload.url) finish(new Error('Missing upload URL.'))
else finish(null, upload.url)
},
})
signal.addEventListener('abort', abort, { once: true })
timer = setTimeout(() => finish(new Error('Upload timed out.')), 120_000)
try {
upload.start()
} catch {
finish(new Error('Could not start upload.'))
}
})
signal.throwIfAborted()
const response = await fetch('/api/upload-publications', {
method: 'POST',
credentials: 'same-origin',
redirect: 'error',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({ uploadUrl }),
signal: AbortSignal.any([signal, AbortSignal.timeout(30_000)]),
})
if (!response.ok || ![200, 201].includes(response.status)) {
throw new Error(`Publication not confirmed (HTTP ${response.status}).`)
}
const receipt = await response.json()
if (typeof receipt.id !== 'string' || !receipt.id) throw new Error('Invalid publication receipt.')
onProgress(100)
return receipt.id
}
Dabei werden die tatsächlichen Optionen und Callbacks von tus-js-client verwendet. Die Frist von zwei Minuten begrenzt die gesamte Übertragung einschließlich Retries. Geht die Antwort auf eine Veröffentlichungsanfrage verloren, ist das Ergebnis unklar. Prüfen Sie die Upload-Liste auf Ihrem Server, bevor Sie einen weiteren Upload starten.
Für eine vollständige Seitenintegration geben Sie die Attribute data-upload-owner und
data-csrf mit korrekt maskierten Werten aus Ihrer sitzungsgestützten Seite auf
<html> aus und ergänzen Sie Folgendes:
<label>File <input id="file" type="file" /></label>
<label><input id="resize" type="checkbox" /> Prepare JPEG or PNG as JPEG</label>
<button id="upload" type="button">Upload</button>
<button id="cancel" type="button">Cancel</button>
<progress id="progress" aria-label="Upload progress" max="100" value="0"></progress>
<p id="status" role="status"></p>
<script type="module" src="/main.js"></script>
Erstellen Sie main.js:
import { prepareImage } from './prepare-image.js'
import { uploadFile } from './upload.js'
const input = document.getElementById('file')
const button = document.getElementById('upload')
const status = document.getElementById('status')
let controller = null
button.onclick = async () => {
if (controller || !input.files?.[0]) return
controller = new AbortController()
button.disabled = true
input.disabled = true
status.textContent = 'Preparing upload…'
try {
let file = input.files[0]
if (document.getElementById('resize').checked) {
file = await prepareImage(file, controller.signal)
}
await uploadFile(file, {
owner: document.documentElement.dataset.uploadOwner,
csrf: document.documentElement.dataset.csrf,
signal: controller.signal,
onProgress(value) {
document.getElementById('progress').value = value
},
})
status.textContent = 'Upload published.'
} catch {
status.textContent = 'Upload failed or canceled. Check your uploads before retrying.'
} finally {
controller = null
button.disabled = false
input.disabled = false
}
}
document.getElementById('cancel').onclick = () => controller?.abort()
Bündeln Sie den Haupteinstiegspunkt und liefern Sie den Worker daneben vom selben HTTPS-Ursprung aus:
corepack yarn esbuild main.js --bundle --format=esm --outfile=public/main.js
cp image-worker.js public/image-worker.js
Uppy für eine reibungslose Nutzererfahrung einsetzen
Uppy bietet eine Oberfläche für Auswahl, Einschränkungen, Fortschritt und Abbruch. Nutzen Sie für
dieses Gateway die Schnittstelle für individuelle Uploader von Uppy, um dieselbe Funktion
uploadFile aufzurufen. So bleibt die Veröffentlichung Teil des erfolgreichen
Abschlusses, und Frist, Authentifizierungsheader und Fehlerbehandlung bleiben gleich. Dies ist ein
alternativer Einstiegspunkt, kein zusätzlicher Uploader, den Sie neben der bisherigen Seite einbinden.
Installieren Sie kompatible Pakete:
corepack yarn add --exact @uppy/core@5.2.0 @uppy/dashboard@5.1.1
Erstellen Sie uppy-main.js und binden Sie es auf einer Seite mit
<div id="drag-drop-area"></div> und denselben Sitzungsattributen ein. Die Anwendung muss beim Entfernen
dieser Oberfläche die zurückgegebene Bereinigungsfunktion aufrufen.
import Uppy from '@uppy/core'
import Dashboard from '@uppy/dashboard'
import '@uppy/core/css/style.min.css'
import '@uppy/dashboard/css/style.min.css'
import { uploadFile } from './upload.js'
export function mountUploader() {
const uppy = new Uppy({
restrictions: { maxNumberOfFiles: 1, maxFileSize: 10 * 1024 * 1024, minFileSize: 1 },
}).use(Dashboard, { inline: true, target: '#drag-drop-area' })
let controller = null
uppy.on('cancel-all', () => controller?.abort())
uppy.on('file-removed', () => controller?.abort())
uppy.addUploader(async (ids) => {
const file = uppy.getFile(ids[0])
if (!file || !(file.data instanceof Blob)) throw new Error('Select a local file.')
controller = new AbortController()
uppy.emit('upload-start', [file])
try {
await uploadFile(file.data, {
owner: document.documentElement.dataset.uploadOwner,
csrf: document.documentElement.dataset.csrf,
signal: controller.signal,
onProgress(value) {
uppy.emit('upload-progress', file, {
uploadStarted: Date.now(),
bytesUploaded: Math.floor((file.data.size * value) / 100),
bytesTotal: file.data.size,
})
},
})
uppy.emit('upload-success', file, { status: 200, body: {} })
} catch {
uppy.emit('upload-error', file, new Error('Upload not confirmed. Check your uploads.'))
throw new Error('Upload not confirmed.')
} finally {
controller = null
}
})
return () => {
controller?.abort()
uppy.destroy()
}
}
let cleanup = mountUploader()
window.addEventListener('pagehide', () => cleanup())
window.addEventListener('pageshow', (event) => {
if (event.persisted) cleanup = mountUploader()
})
Bündeln Sie uppy-main.js wie oben mit esbuild und binden Sie sowohl das erzeugte
JavaScript als auch das CSS in Ihre Seite ein. Uploads von Remote-Anbietern benötigen eine separate,
authentifizierte Companion-Integration; dieses Beispiel akzeptiert nur lokale Blobs. Weitere
Informationen finden Sie in der Uploader-API von Uppy.
Fazit
Beginnen Sie mit begrenzten Dateigrößen, sequenziellen Blöcken, expliziter Fehlerbehandlung und einer vom Server bestätigten Veröffentlichung. Ergänzen Sie die Bildvorbereitung, wenn die Qualitätseinbußen vertretbar sind. Messen Sie Übertragungszeit, Vorbereitungszeit, Retries und Abschluss unabhängig voneinander, bevor Sie die Einstellungen optimieren.
Weiterführende Ressourcen
Antworten auf häufige Fragen
Wie bestätigen Sie einen Upload? Die Anzahl übertragener Bytes reicht nicht aus. Prüfen Sie nach Validierung und Veröffentlichung eine authentifizierte Empfangsbestätigung des Servers. Eine anbieterspezifische Malware-Analyse erfordert eine separate Integration; dafür gibt es keinen universellen CLI-Befehl zur Upload-Verifizierung.
Was ist ein uneingeschränkter Datei-Upload? Gemeint ist ein Upload-Endpunkt, der Inhalte ohne angemessene Einschränkungen akzeptiert und dadurch möglicherweise das Speichern oder Ausführen gefährlicher Dateien ermöglicht. Validieren Sie auf dem Server und trennen Sie den Speicher von ausführbaren Webinhalten.
Welche HTML-Formulardatencodierung unterstützt Datei-Uploads? Ein klassisches Formular verwendet
method="post" und enctype="multipart/form-data" sowie ein Dateiauswahlfeld mit Namen.
Wenn Sie FormData mit Fetch oder Axios im Browser senden, lassen Sie den Browser
die Multipart-Grenze setzen; erstellen Sie den Header nicht selbst.
