Verschlüsselungs-Performance-Optimierung: schnelle Krypto im Browser
Optimieren Sie die Verschlüsselungsleistung für große Transfers. Streaming-Verschlüsselung, Web Workers und Chunk-Verarbeitung.
Eine 5-GB-Datei im Browser zu verschlüsseln, ohne die Oberfläche einzufrieren, erfordert eine bestimmte Kombination von Techniken: die Web Crypto API für hardwarebeschleunigte AES-256-GCM-Verschlüsselung (1–2 GB/s mit AES-NI), Chunk-Verarbeitung in 1–4-MB-Blöcken, um den Speicher zu begrenzen, Web Workers, damit der Haupt-Thread reaktionsfähig bleibt, Streaming-Reads via File.stream() statt FileReader.readAsArrayBuffer(), und sorgfältiges Nonce-Management, damit Chunks parallel verschlüsselt werden können. Reine JavaScript-Krypto-Bibliotheken laufen 10–20× langsamer als Web Crypto und sollten nur für Primitive reserviert werden, die der Browser nicht nativ bereitstellt. So erreichen Sie auf echter Nutzerhardware Durchsatz im Bereich mehrerer Hundert Megabyte pro Sekunde.
Zuerst Basismessungen durchführen
Vor dem Optimieren messen. Auf einem 2024 MacBook Air M2 in Chrome 120 läuft die Verschlüsselung eines 1-GB-Puffers mit AES-256-GCM via Web Crypto in einer engen Schleife bei ~1,7 GB/s. Dieselbe Operation auf einem mittleren Android-Gerät (Pixel 7) läuft bei ~600 MB/s. Ein Intel-Laptop von 2015 ohne Hardware-AES erreicht ~250 MB/s.
Was das bedeutet: Web Crypto AES-GCM ist für die meisten verschlüsselten Dateiübertragungs-Flows nicht der Engpass. Der Engpass ist üblicherweise das Dateilesen, JavaScript-Marshalling zwischen ArrayBuffers oder der Netzwerk-Upload. Optimieren Sie zuerst dort.
Benchmarks, die Sie in der App ausführen:
const blob = new Uint8Array(1024 * 1024 * 100); // 100 MB
const key = await crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 }, true, ["encrypt"]
);
const iv = crypto.getRandomValues(new Uint8Array(12));
const start = performance.now();
await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, blob);
console.log(`${(100 / (performance.now() - start) * 1000).toFixed(0)} MB/s`);
Führen Sie das in Chrome, Firefox und Safari aus. Chrome auf Apple Silicon ist am schnellsten; Firefox etwas langsamer; Safari auf Intel leicht dahinter. Mobile erreicht grob 30–50 % der Desktop-Geschwindigkeit.
Web Crypto statt JavaScript-Bibliotheken verwenden
Für AES-GCM, AES-CBC, PBKDF2, HMAC, RSA, ECDH, ECDSA und SHA-256/384/512 nutzt die native Web Crypto API des Browsers Hardwarebeschleunigung, wo verfügbar (AES-NI auf x86, ARMv8-Crypto-Extensions auf Mobile). JavaScript-Bibliotheken wie @noble/ciphers oder reines crypto-js können auf diese Instruktionen nicht zugreifen und laufen rein im Interpreter.
Typisches Geschwindigkeitsverhältnis für AES-256-GCM auf 1-MB-Eingaben:
- Web Crypto (Hardwarebeschleunigung): 1–2 GB/s
- libsodium.js WASM: 400–800 MB/s
- @noble/ciphers reines JS: 100–200 MB/s
- crypto-js reines JS: 30–80 MB/s
Für eine 5-GB-Datei ist der Unterschied zwischen Web Crypto und reinem JS grob 3 Sekunden gegenüber ~50 Sekunden. Nutzersichtbar. Bevorzugen Sie immer Web Crypto für das, was es unterstützt. Verwenden Sie WASM-Bibliotheken (libsodium.js, argon2-browser) nur für Algorithmen, die Web Crypto nicht hat (ChaCha20-Poly1305, Argon2id, X25519 in älteren Browsern).
Chunk-Verarbeitung für große Dateien
Dateien über ~500 MB passen auf den meisten Geräten nicht bequem in einen einzigen ArrayBuffer. Teilen Sie sie in 1–4-MB-Stücke auf und verschlüsseln Sie jedes.
const CHUNK_SIZE = 4 * 1024 * 1024; // 4 MB
async function encryptLargeFile(file, key) {
const chunks = [];
let chunkIndex = 0;
for (let offset = 0; offset < file.size; offset += CHUNK_SIZE) {
const chunk = await file.slice(offset, offset + CHUNK_SIZE).arrayBuffer();
const iv = new Uint8Array(12);
new DataView(iv.buffer).setBigUint64(4, BigInt(chunkIndex++));
const ct = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv }, key, chunk
);
chunks.push({ iv, ct: new Uint8Array(ct) });
}
return chunks;
}
Warum 4-MB-Chunks? Kleinere Chunks (z. B. 64 KB) verursachen per-Aufruf-Overhead aus der Web Crypto API, der für kleine Eingaben dominiert. Größere Chunks (z. B. 64 MB) passen nicht gut in L2/L3-Cache und haben schlechteren Speicherdruck. 1–4 MB ist der Sweet Spot für Desktop und Mobile gleichermaßen.
Web Workers für eine reaktionsfähige Oberfläche
Verschlüsselung im Haupt-Thread blockiert Rendering und Eingabe. Für alles über ~500 ms Arbeit lagern Sie in einen Web Worker aus.
// worker.js
self.onmessage = async (e) => {
const { chunk, key, iv } = e.data;
const ciphertext = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv }, key, chunk
);
self.postMessage(ciphertext, [ciphertext]);
};
// Haupt-Thread
const worker = new Worker('/worker.js');
worker.postMessage({ chunk, key, iv }, [chunk]);
worker.onmessage = (e) => { /* CT verarbeiten */ };
Übertragen Sie ArrayBuffers mit dem zweiten Argument an postMessage — das verschiebt die Eigentümerschaft (zero-copy), anstatt zu klonen. Ohne Transfer fügt das Klonen eines 4-MB-Chunks ~20 ms Overhead pro Chunk hinzu.
Die Web Crypto API ist in Workers verfügbar, sodass die eigentliche Verschlüsselung dort mit identischer Performance zum Haupt-Thread läuft.
Parallele Chunk-Verschlüsselung
Per-Chunk-AES-GCM-Verschlüsselung ist unabhängig, solange Nonces nicht kollidieren. Leiten Sie Nonces deterministisch vom Chunk-Index ab, können Sie Chunks parallel über mehrere Workers verschlüsseln. Diminishing Returns setzen nach ~4 Workers auf typischer Hardware ein, weil Web Crypto so schnell ist, dass der Engpass auf Dateilesen und Inter-Thread-Nachrichtenübertragung verschoben wird. Benchmarken Sie, bevor Sie sich auf Komplexität einlassen; manchmal ist sequenzielle Single-Worker-Verschlüsselung genauso schnell wie parallele Multi-Worker-Verschlüsselung aufgrund des Overheads.
Streaming mit Readable Streams
Für wirklich große Dateien (20+ GB) vermeiden Sie das Laden von Chunks auf einmal in den Speicher. Verwenden Sie File.stream():
const reader = file.stream().getReader();
const writer = uploadStream.getWriter();
let chunkIndex = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
const iv = new Uint8Array(12);
new DataView(iv.buffer).setBigUint64(4, BigInt(chunkIndex++));
const ct = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv }, key, value
);
await writer.write(new Uint8Array(ct));
}
await writer.close();
Das hält die Speichernutzung auf einen Chunk gleichzeitig begrenzt. Der Browser liest von der Disk, verschlüsselt, schreibt in den Upload-Stream, liest den nächsten Chunk. Die Speichernutzung bleibt unabhängig von der Dateigröße unter 10 MB.
Upload-Parallelität
Verschlüsselung läuft parallel zum Upload, nicht seriell. Während Chunk N hochgeladen wird, wird Chunk N+1 verschlüsselt. Verwenden Sie eine Pipeline:
const encryptQueue = [];
const uploadQueue = [];
const MAX_IN_FLIGHT = 4;
// Encryptor produziert, Uploader konsumiert
async function pipeline() {
// Starten Sie die Verschlüsselung der ersten Chunks
for (let i = 0; i < MAX_IN_FLIGHT; i++) startEncrypt(i);
// Mit jedem Abschluss Upload starten und nächste Verschlüsselung einreihen
// (begrenzte Warteschlange hält Speichernutzung vorhersagbar)
}
TCP-Slow-Start und TLS-Handshake-Overhead bedeuten, dass initiale Uploads langsam sind. 4–8 gleichzeitige Uploads an eine einzelne Origin zu halten, hält die Pipe voll, ohne Browser-Limits auszulösen (6 Verbindungen pro Origin in Chrome/Firefox).
WebAssembly für fehlende Primitive
Für Argon2id-Schlüsselableitung oder ChaCha20-Poly1305 hat Web Crypto keine native Unterstützung. WASM-Bibliotheken füllen die Lücke:
- libsodium.js bietet Argon2id, XChaCha20-Poly1305,
crypto_secretstream - argon2-browser bietet nur Argon2, aber kleineres Bundle
- @noble/hashes bietet reines JS Argon2 (langsamer) mit winzigem Bundle
WASM-Versionen erreichen 60–80 % nativer Geschwindigkeit für die meisten Krypto-Workloads. Für Dateiübertragungen ist eine 1–2-sekündige Argon2id-Ableitung bei Upload und Download akzeptabel; eine 5-sekündige reine JS-Ableitung nicht.
Laden Sie WASM dynamisch, damit es das initiale Seiten-Rendering nicht blockiert:
const sodium = await import('libsodium-wrappers');
await sodium.ready;
Fortschrittsanzeige
Große Verschlüsselungen brauchen Fortschritts-Feedback, sonst gehen Nutzer davon aus, dass die App eingefroren ist. Zählen Sie verschlüsselte Bytes und posten Sie Fortschrittsereignisse:
let processed = 0;
for await (const chunk of chunks) {
await encryptChunk(chunk);
processed += chunk.size;
onProgress({ done: processed, total: file.size, pct: processed / file.size });
}
Drosseln Sie UI-Updates auf ~10 Hz via requestAnimationFrame oder eine einfache Zeitstempelprüfung; häufigere Updates verschwenden Zyklen bei Neuzeichnungen, die Menschen nicht wahrnehmen können.
Speicher-Obergrenze und GC-Druck
Jeder ArrayBuffer lebt, bis er nicht mehr referenziert wird. 20 verschlüsselte Chunks von je 4 MB zu halten bedeutet ~80 MB gebundenen Speicher. Auf mobilen Browsern mit engen Speicherlimits (iOS Safari begrenzt auf ~200–400 MB pro Tab) ist das relevant.
Geben Sie Referenzen zeitnah frei:
for (let i = 0; i < chunks.length; i++) {
const chunk = chunks[i];
chunks[i] = null; // GC kann zurückfordern
const ct = await encrypt(chunk);
await upload(ct);
}
Halten Sie kein vollständiges Array verschlüsselter Chunks im Speicher; streamen Sie sie zum Uploader und lassen Sie Referenzen los, während Sie weitergehen.
Reale Zielwerte
Für eine 1-GB-verschlüsselte Dateiübertragung in einem Browser-Tab: Verschlüsselungszeit 1–3 Sekunden (hardwarebeschleunigt), Upload-Zeit 30 Sekunden bei einer 300-Mbit/s-Verbindung, insgesamt ~35 Sekunden (hauptsächlich netzwerkgebunden), Speicher unter 50 MB Peak mit korrektem Streaming, Oberfläche durchgehend reaktionsfähig (Haupt-Thread niemals länger als 50 ms blockiert). Verfehlen Sie diese Ziele, und Nutzer bemerken es. HexaTransfers 10-GB-Grenze ist im Browser erreichbar, weil Web Crypto plus Chunk-Streaming den gesamten Pfad effizient hält.
Auf hexatransfer.com testen — kostenlos, ohne Konto, bis 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