Optimisation des performances de chiffrement : crypto rapide dans le navigateur
Optimisez les performances de chiffrement pour les gros transferts. Chiffrement streaming, Web Workers et techniques de traitement par morceaux.
Chiffrer un fichier de 5 Go dans un navigateur sans tuer l'interface nécessite un ensemble précis de techniques : l'API Web Crypto pour AES-256-GCM accéléré par matériel (1 à 2 Go/s avec AES-NI), le traitement par morceaux de 1 à 4 Mo pour borner la mémoire, des Web Workers pour garder le thread principal réactif, des lectures en streaming via File.stream() plutôt que FileReader.readAsArrayBuffer(), et une gestion soigneuse des nonces pour que les morceaux puissent être chiffrés en parallèle. Les bibliothèques crypto JavaScript pur tournent 10 à 20 fois plus lentement que Web Crypto et doivent être réservées aux primitives que le navigateur n'expose pas nativement. Voici comment atteindre un débit de plusieurs centaines de mégaoctets par seconde sur du matériel utilisateur réel.
Commencez par des mesures de référence
Avant d'optimiser, mesurez. Sur un MacBook Air M2 de 2024 dans Chrome 120, chiffrer un buffer de 1 Go avec AES-256-GCM via Web Crypto en boucle serrée tourne à ~1,7 Go/s. La même opération sur un téléphone Android milieu de gamme (Pixel 7) tourne à ~600 Mo/s. Un ordinateur portable Intel de 2015 avec AES uniquement logiciel atteint ~250 Mo/s.
Ce que cela vous dit : Web Crypto AES-GCM n'est pas le goulot d'étranglement pour la plupart des flux de transfert de fichiers chiffrés. Le goulot est généralement la lecture de fichier, le marshaling JavaScript entre ArrayBuffers, ou le téléversement réseau. Optimisez ceux-là d'abord.
Benchmarks à exécuter dans l'application :
const blob = new Uint8Array(1024 * 1024 * 100); // 100 Mo
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)} Mo/s`);
Exécutez sur Chrome, Firefox, Safari. Chrome sur Apple Silicon sera le plus rapide ; Firefox légèrement plus lent ; Safari sur Intel légèrement derrière. Le mobile tourne à environ 30-50 % de la vitesse bureau.
Utilisez Web Crypto, pas les bibliothèques JavaScript
Pour AES-GCM, AES-CBC, PBKDF2, HMAC, RSA, ECDH, ECDSA et SHA-256/384/512, l'API Web Crypto native du navigateur utilise l'accélération matérielle quand elle est disponible (AES-NI sur x86, extensions crypto ARMv8 sur mobile). Les bibliothèques JavaScript comme @noble/ciphers ou crypto-js JavaScript pur ne peuvent pas accéder à ces instructions et tournent entièrement dans l'interpréteur.
Ratio de vitesse typique pour AES-256-GCM sur des entrées de 1 Mo :
- Web Crypto (accélération matérielle) : 1-2 Go/s
- libsodium.js WASM : 400-800 Mo/s
- @noble/ciphers JavaScript pur : 100-200 Mo/s
- crypto-js JavaScript pur : 30-80 Mo/s
Pour un fichier de 5 Go, la différence entre Web Crypto et JavaScript pur est ~3 secondes contre ~50 secondes. Perceptible par l'utilisateur. Préférez toujours Web Crypto pour ce qu'il supporte. Utilisez les bibliothèques WASM (libsodium.js, argon2-browser) uniquement pour les algorithmes que Web Crypto n'a pas (ChaCha20-Poly1305, Argon2id, X25519 sur les navigateurs plus anciens).
Traitement par morceaux pour les gros fichiers
Les fichiers de plus de ~500 Mo ne tiennent pas confortablement dans un seul ArrayBuffer sur la plupart des appareils. Découpez-les en morceaux de 1 à 4 Mo et chiffrez chaque morceau.
const CHUNK_SIZE = 4 * 1024 * 1024; // 4 Mo
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;
}
Pourquoi des morceaux de 4 Mo ? Les petits morceaux (par ex. 64 Ko) entraînent un overhead par appel de l'API Web Crypto qui domine pour les petites entrées. Les morceaux plus grands (par ex. 64 Mo) ne tiennent pas bien dans le cache L2/L3 et ont une pression mémoire plus importante. 1 à 4 Mo tend à être le point optimal sur bureau et mobile.
Web Workers pour garder l'interface réactive
Le chiffrement sur le thread principal bloque le rendu et les entrées. Pour tout ce qui dépasse ~500 ms de travail, déchargez vers un Web Worker.
// 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]);
};
// thread principal
const worker = new Worker('/worker.js');
worker.postMessage({ chunk, key, iv }, [chunk]);
worker.onmessage = (e) => { /* gérer le texte chiffré */ };
Transférez les ArrayBuffers avec le second argument de postMessage — cela déplace la propriété (zéro copie) plutôt que de cloner. Sans transfert, le clonage d'un morceau de 4 Mo ajoute ~20 ms d'overhead par morceau.
L'API Web Crypto est disponible dans les Workers, donc le chiffrement réel s'y exécute avec des performances identiques au thread principal.
Chiffrement parallèle des morceaux
Le chiffrement AES-GCM par morceau est indépendant tant que les nonces ne se heurtent pas. Dérivez les nonces de façon déterministe à partir de l'index du morceau et vous pouvez chiffrer des morceaux en parallèle sur plusieurs Workers. Les rendements décroissants apparaissent après ~4 workers sur du matériel typique car Web Crypto est si rapide que le goulot se déplace vers la lecture de fichier et le passage de messages entre threads. Comparez avant de vous engager dans la complexité ; parfois le chiffrement séquentiel à un seul worker est aussi rapide que le parallèle multi-workers en raison de l'overhead.
Streaming avec les flux lisibles
Pour les fichiers vraiment volumineux (20+ Go), évitez de charger même des morceaux en mémoire tous à la fois. Utilisez 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();
Cela maintient l'utilisation mémoire bornée à un morceau à la fois. Le navigateur lit depuis le disque, chiffre, écrit dans le flux de téléversement, lit le morceau suivant. L'utilisation mémoire reste sous 10 Mo quelle que soit la taille du fichier.
Concurrence du téléversement
Le chiffrement s'exécute en parallèle avec le téléversement, pas en série. Pendant que le morceau N est téléversé, le morceau N+1 est en cours de chiffrement. Utilisez un pipeline :
const MAX_IN_FLIGHT = 4;
// Le chiffreur produit, le téléverseur consomme
async function pipeline() {
// Démarrer le chiffrement des premiers morceaux
for (let i = 0; i < MAX_IN_FLIGHT; i++) startEncrypt(i);
// Au fur et à mesure que chaque morceau se complète, démarrer le téléversement
// et mettre en file le chiffrement suivant
// (la file bornée garde l'utilisation mémoire prévisible)
}
Le slow-start TCP et l'overhead du handshake TLS signifient que les téléversements initiaux sont lents. Maintenir 4 à 8 téléversements concurrents vers une seule origine garde le tuyau plein sans dépasser les limites du navigateur (6 connexions par origine dans Chrome/Firefox).
WebAssembly pour les primitives manquantes
Pour la dérivation de clés Argon2id ou ChaCha20-Poly1305, Web Crypto n'a pas de support natif. Les bibliothèques WASM comblent le manque :
- libsodium.js fournit Argon2id, XChaCha20-Poly1305,
crypto_secretstream - argon2-browser fournit uniquement Argon2 mais avec un bundle plus petit
- @noble/hashes fournit un Argon2 JavaScript pur (plus lent) avec un tout petit bundle
Les versions WASM atteignent 60 à 80 % de la vitesse native pour la plupart des charges de travail crypto. Pour le transfert de fichiers, une dérivation Argon2id de 1 à 2 secondes lors du téléversement et du téléchargement est acceptable ; une dérivation JavaScript pur de 5 secondes ne l'est pas.
Chargez WASM de façon dynamique pour ne pas bloquer le rendu initial de la page :
const sodium = await import('libsodium-wrappers');
await sodium.ready;
Rapport de progression
Les gros chiffrements nécessitent un retour de progression ou les utilisateurs supposent que l'application a gelé. Comptez les octets chiffrés et publiez des événements de progression :
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 });
}
Limitez les mises à jour UI à ~10 Hz via requestAnimationFrame ou une vérification d'horodatage simple ; des mises à jour plus fréquentes gaspillent des cycles sur des repeints que les humains ne peuvent pas voir.
Plafond mémoire et pression sur le GC
Chaque ArrayBuffer vit jusqu'à ce qu'il ne soit plus référencé. Conserver 20 morceaux chiffrés de 4 Mo chacun signifie ~80 Mo de mémoire épinglée. Sur les navigateurs mobiles avec des limites mémoire serrées (iOS Safari plafonne à ~200-400 Mo par onglet), cela compte.
Libérez les références rapidement :
for (let i = 0; i < chunks.length; i++) {
const chunk = chunks[i];
chunks[i] = null; // laisser le GC récupérer
const ct = await encrypt(chunk);
await upload(ct);
}
Évitez de garder un tableau complet de morceaux chiffrés en mémoire ; transmettez-les au téléverseur et abandonnez les références au fur et à mesure.
Cibles réelles
Pour un transfert de fichier chiffré de 1 Go dans un onglet navigateur : temps de chiffrement 1 à 3 secondes (accéléré par matériel), temps de téléversement 30 secondes sur une connexion à 300 Mbps, total environ 35 secondes en temps réel (principalement limité par le réseau), mémoire sous 50 Mo au pic avec un streaming correct, interface réactive tout au long (thread principal jamais bloqué plus de 50 ms). Si vous ratez ces cibles, les utilisateurs le remarquent. Le plafond de 10 Go de HexaTransfer est réalisable dans le navigateur car Web Crypto plus le streaming par morceaux maintient tout le chemin efficacement.
Essayez sur hexatransfer.com — gratuit, sans compte, jusqu'à 10 Go.
Envoyez vos fichiers volumineux en toute sécurité avec le chiffrement de bout en bout
Transférez des fichiers jusqu'à 10 Go gratuitement avec le chiffrement de bout en bout. Aucun compte requis. Vos fichiers sont chiffrés dans votre navigateur avant l'envoi — personne d'autre ne peut les lire.
Envoyer un fichier