JS-Datei-Uploads mit Web Workers beschleunigen
Die Dateiverarbeitung kann dazu führen, dass eine Upload-Oberfläche nicht mehr reagiert. Web Workers verlagern JavaScript-Arbeit vom Haupt-Thread weg und halten ihn so für Eingaben und Rendering frei. Die Bandbreite der Verbindung erhöhen sie nicht: Netzwerk-Uploads laufen bereits asynchron. Messen Sie den Overhead für Worker-Start und Nachrichtenaustausch gegen die CPU-Arbeit, die Sie erledigen müssen.
Einführung in Web Workers und ihre Vorteile
Worker eignen sich für Aufgaben wie Bildtransformationen oder Dateianalysen. Sie haben keinen Zugriff auf das DOM der Seite, daher verbindet ein kleines Nachrichtenprotokoll die Verarbeitung mit der sichtbaren Oberfläche. Halten Sie die Anzahl der Worker begrenzt und geben Sie sie frei, sobald ein Vorgang abgeschlossen oder abgebrochen wird.
Dieses Beispiel verwendet TypeScript mit einem Bundler wie Vite. Typannotationen werden beim Build entfernt; Browser führen die resultierenden JavaScript-Module aus. Wir behalten eine einzige Worker-Implementierung bei, statt getrennte JavaScript- und TypeScript-Kopien zu pflegen. Die SHA-256-Berechnung veranschaulicht einen Verarbeitungsschritt für kleine Dateien, doch Web Crypto ist selbst asynchron und ist kein Beleg dafür, dass ein Upload schneller wird, wenn man ihn in einen Worker verlagert.
Beginnen Sie mit einem Vanilla-TypeScript-Projekt von Vite oder installieren Sie die Build-Tools in einem bestehenden Projekt:
yarn add --dev typescript vite
Einen einfachen Datei-Upload in JavaScript einrichten
Fügen Sie dieses Formular in Ihrem Vite-Projekt zu index.html hinzu. Liefern Sie es in der Entwicklung
über localhost aus, in der Produktion über HTTPS; öffnen Sie Worker-Beispiele nicht über eine URL mit
dem Schema file:.
<label for="fileInput">File to upload</label>
<input type="file" id="fileInput" />
<button type="button" id="uploadBtn">Upload</button>
<button type="button" id="cancelBtn" disabled>Cancel</button>
<label for="uploadProgress">Upload progress</label>
<progress id="uploadProgress" value="0" max="100"></progress>
<p id="status" role="status"></p>
<script type="module" src="/src/main.ts"></script>
Die Server-Endpunkte in diesem Tutorial sind Schnittstellenvereinbarungen der Anwendung, keine in Vite integrierten Routen. /upload
nimmt file als Multipart entgegen. /upload-chunk nimmt chunk, uploadId, chunkIndex, totalChunks
und fileName entgegen; /complete-upload nimmt JSON mit uploadId und totalChunks entgegen. Jeder
Endpunkt gibt erst dann einen erfolgreichen HTTP-Status zurück, wenn er den entsprechenden Vorgang
angenommen hat. Response-Bodies werden nicht verwendet.
Authentifizieren Sie Anfragen und autorisieren Sie jede Upload-ID, erzwingen Sie Limits, speichern Sie Chunks idempotent und prüfen Sie die Vollständigkeit vor der Finalisierung. Machen Sie aus dem übergebenen Dateinamen niemals einen ungeprüften Speicherpfad. Lassen Sie verwaiste Uploads ablaufen. Wenn Sie statt dieser zu Lehrzwecken gedachten Schnittstellenvereinbarung ein gepflegtes, fortsetzbares Protokoll möchten, verwenden Sie tus mit einem kompatiblen Server.
Web Workers für die Dateiverarbeitung integrieren
Erstellen Sie src/upload.worker.ts. Jeder Worker bearbeitet einen Vorgang. Kleine Dateien werden vor dem
Upload gehasht; größere Dateien überspringen das Hashing der gesamten Datei und verwenden
sequenzielle Chunks von 5 MiB. Fehler werden zu bereinigten Meldungen, und der Haupt-Thread beendet
den Worker nach Abschluss oder Fehlschlag.
import type { WorkerMessage, WorkerResponse } from './worker-types.ts'
declare const self: DedicatedWorkerGlobalScope
const CHUNK_SIZE = 5 * 1024 * 1024
function send(message: WorkerResponse): void {
self.postMessage(message)
}
function uploadRequest(body: FormData, endpoint: string, onProgress: (ratio: number) => void): Promise<void> {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.open('POST', endpoint)
xhr.timeout = 60_000
xhr.upload.onprogress = (event) => {
if (event.lengthComputable) onProgress(event.loaded / event.total)
}
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) resolve()
else reject(new Error('Upload request rejected'))
}
xhr.onerror = () => reject(new Error('Upload network error'))
xhr.ontimeout = () => reject(new Error('Upload timed out'))
xhr.onabort = () => reject(new Error('Upload canceled'))
xhr.send(body)
})
}
async function run({ file }: WorkerMessage): Promise<void> {
if (file.size === 0) throw new Error('Empty file')
if (file.size <= CHUNK_SIZE) {
const hash = await crypto.subtle.digest('SHA-256', await file.arrayBuffer())
const sha256 = Array.from(new Uint8Array(hash), (byte) => byte.toString(16).padStart(2, '0')).join('')
send({ type: 'processed', sha256 })
const body = new FormData()
body.append('file', file)
await uploadRequest(body, '/upload', (ratio) => send({ type: 'progress', percent: ratio * 100 }))
} else {
const uploadId = crypto.randomUUID()
const totalChunks = Math.ceil(file.size / CHUNK_SIZE)
for (let index = 0; index < totalChunks; index++) {
const start = index * CHUNK_SIZE
const chunk = file.slice(start, start + CHUNK_SIZE)
const body = new FormData()
body.append('chunk', chunk)
body.append('uploadId', uploadId)
body.append('chunkIndex', String(index))
body.append('totalChunks', String(totalChunks))
body.append('fileName', file.name)
await uploadRequest(body, '/upload-chunk', (ratio) => {
send({ type: 'progress', percent: (start + ratio * chunk.size) / file.size * 100 })
})
}
const response = await fetch('/complete-upload', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ uploadId, totalChunks }),
signal: AbortSignal.timeout(60_000),
})
if (!response.ok) throw new Error('Finalization failed')
}
send({ type: 'complete' })
}
self.onmessage = (event: MessageEvent<WorkerMessage>) => {
run(event.data).catch(() => send({ type: 'error', message: 'Upload failed. Please try again.' }))
}
TypeScript-Unterstützung für Web Workers
Erstellen Sie src/worker-types.ts für beide Seiten der Nachrichtenvereinbarung:
export interface WorkerMessage {
file: File
}
export type WorkerResponse =
| { type: 'processed'; sha256: string }
| { type: 'progress'; percent: number }
| { type: 'complete' }
| { type: 'error'; message: string }
Prüfen Sie Dateien des Haupt-Threads und des Workers getrennt, damit TypeScript nicht
widersprüchliche globale DOM- und Worker-Objekte kombiniert. Verwenden Sie für die Seite diese
tsconfig.json:
{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "Bundler",
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"types": [],
"strict": true,
"verbatimModuleSyntax": true,
"allowImportingTsExtensions": true,
"noEmit": true
},
"include": ["src/main.ts", "src/worker-types.ts"]
}
Fügen Sie dann tsconfig.worker.json hinzu:
{
"extends": "./tsconfig.json",
"compilerOptions": { "lib": ["ES2022", "WebWorker"] },
"include": ["src/upload.worker.ts", "src/worker-types.ts"]
}
Führen Sie beide Prüfungen aus. Vite erledigt den Build des Modul-Workers getrennt von der Typprüfung:
yarn tsc --project tsconfig.json
yarn tsc --project tsconfig.worker.json
yarn vite
Große Datei-Uploads effizient handhaben
Der Chunk-Zweig des Workers schneidet jeweils nur einen Chunk heraus, statt die gesamte Datei im
Speicher zu materialisieren. Der Worker wählt diesen Zweig für Dateien, die größer sind als das in
CHUNK_SIZE festgelegte Limit. Der Fortschritt basiert auf Byte-Zählungen, daher fällt ein kurzer
letzter Chunk nicht so stark ins Gewicht wie ein vollständiger Chunk.
Dieses Beispiel enthält keine Logik für Retries oder für das Fortsetzen nach einem Neuladen. Eine fehlgeschlagene Anfrage stoppt den Vorgang, und ein neuer Versuch erhält eine neue Upload-ID. Behaupten Sie nicht, ein Upload sei fortsetzbar, nur weil eine Datei in Chunks aufgeteilt wird. Ein Abbruch stoppt die Aktivität auf dem Client, kann aber bereits vom Server angenommene Bytes nicht rückgängig machen; ein Ablauf im Backend oder ein expliziter, autorisierter Endpunkt zum Abbrechen muss diese bereinigen.
Robuste Fortschrittsanzeigen und Fehlerbehandlung implementieren
Erstellen Sie src/main.ts. Das ausgewählte Objekt File wird einmal pro Klick erfasst und nach der
Verarbeitung nicht erneut gelesen. Die Bedienelemente verhindern überlappende Durchläufe, und Erfolg,
Fehlschlag sowie Abbruch geben jeweils den Worker frei.
import type { WorkerMessage, WorkerResponse } from './worker-types.ts'
const fileInput = document.getElementById('fileInput')
const uploadBtn = document.getElementById('uploadBtn')
const cancelBtn = document.getElementById('cancelBtn')
const uploadProgress = document.getElementById('uploadProgress')
const statusElement = document.getElementById('status')
if (!(fileInput instanceof HTMLInputElement) || !(uploadBtn instanceof HTMLButtonElement)
|| !(cancelBtn instanceof HTMLButtonElement) || !(uploadProgress instanceof HTMLProgressElement)
|| !(statusElement instanceof HTMLElement)) {
throw new Error('Missing upload controls')
}
let worker: Worker | null = null
const finish = (message: string): void => {
worker?.terminate()
worker = null
uploadBtn.disabled = false
fileInput.disabled = false
cancelBtn.disabled = true
statusElement.textContent = message
}
uploadBtn.addEventListener('click', () => {
const file = fileInput.files?.[0]
if (!file || file.size === 0) {
statusElement.textContent = 'Please select a nonempty file.'
return
}
if (worker) return
uploadProgress.value = 0
uploadBtn.disabled = true
fileInput.disabled = true
cancelBtn.disabled = false
statusElement.textContent = 'Preparing upload.'
try {
worker = new Worker(new URL('./upload.worker.ts', import.meta.url), { type: 'module' })
worker.onmessage = (event: MessageEvent<WorkerResponse>) => {
const response = event.data
switch (response.type) {
case 'processed':
statusElement.textContent = 'File processed. Uploading.'
break
case 'progress':
uploadProgress.value = response.percent
statusElement.textContent = `Uploading: ${Math.round(response.percent)}%`
break
case 'complete':
uploadProgress.value = 100
finish('Upload complete.')
break
case 'error':
finish(response.message)
break
}
}
worker.onerror = (event) => {
event.preventDefault()
finish('The upload worker failed. Please try again.')
}
worker.onmessageerror = () => finish('Could not read the upload worker response.')
worker.postMessage({ file } satisfies WorkerMessage)
} catch {
finish('Could not start the upload worker.')
}
})
cancelBtn.addEventListener('click', () => finish('Upload canceled.'))
window.addEventListener('pagehide', () => finish('Upload stopped.'))
Testen Sie mit einer kleinen Datei, einer Datei, die größer als ein Chunk ist, einer fehlgeschlagenen Finalisierung, einem Abbruch und einem zweiten Upload. Eine erfolgreiche Worker-Nachricht bedeutet, dass der Server die Upload-Vereinbarung angenommen hat, nicht, dass die Datei den Virenscan oder eine andere Verarbeitung bestanden hat. Halten Sie diese Zustände in einer produktiven Oberfläche auseinander.
Als gepflegter Dateiuploader mit Fortschrittsanzeige und fortsetzbaren Übertragungen kann Uppy den Upload-Workflow übernehmen, während Sie Worker für die Verarbeitung reservieren, die tatsächlich davon profitiert.
