Optimização do desempenho de encriptação: criptografia rápida no navegador
Optimize o desempenho da encriptação para transferências de ficheiros grandes. Encriptação em streaming, Web Workers e técnicas de processamento em blocos para criptografia rápida.
Encriptar um ficheiro de 5 GB num navegador sem destruir a interface requer um conjunto específico de técnicas: Web Crypto API para AES-256-GCM com aceleração hardware (1-2 GB/s com AES-NI), processamento em blocos de 1-4 MB para limitar a memória, Web Workers para manter a thread principal responsiva, leituras em streaming via File.stream() em vez de FileReader.readAsArrayBuffer(), e gestão cuidadosa de nonces para que os blocos possam ser encriptados em paralelo. As bibliotecas de criptografia em JavaScript puro são 10-20x mais lentas que a Web Crypto e devem ser reservadas para primitivos que o navegador não expõe nativamente. Eis como atingir centenas de megabytes por segundo em hardware real de utilizadores.
Primeiro, meça a linha de base
Antes de optimizar, meça. Num MacBook Air M2 de 2024 no Chrome 120, encriptar um buffer de 1 GB com AES-256-GCM via Web Crypto num ciclo apertado corre a ~1,7 GB/s. A mesma operação num telemóvel Android de gama média (Pixel 7) corre a ~600 MB/s. Um portátil Intel de 2015 sem aceleração hardware de AES atinge ~250 MB/s.
O que isto revela: a Web Crypto AES-GCM não é o gargalo para a maioria dos fluxos de transferência de ficheiros encriptados. O gargalo é normalmente a leitura do ficheiro, o marshaling JavaScript entre ArrayBuffers ou o upload de rede. Optimize esses primeiro.
Benchmarks a executar na aplicação:
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`);
Execute no Chrome, Firefox, Safari. O Chrome no Apple Silicon será o mais rápido; Firefox ligeiramente mais lento; Safari em Intel um pouco atrás. Os dispositivos móveis correm a aproximadamente 30-50% da velocidade do desktop.
Use Web Crypto, não bibliotecas JavaScript
Para AES-GCM, AES-CBC, PBKDF2, HMAC, RSA, ECDH, ECDSA e SHA-256/384/512, a Web Crypto API nativa do navegador usa aceleração hardware quando disponível (AES-NI em x86, extensões crypto ARMv8 em dispositivos móveis). As bibliotecas JavaScript como @noble/ciphers ou crypto-js puro não conseguem aceder a estas instruções e correm puramente no interpretador.
Rácio de velocidade típico para AES-256-GCM em inputs de 1 MB:
- Web Crypto (aceleração hardware): 1-2 GB/s
- libsodium.js WASM: 400-800 MB/s
- @noble/ciphers JS puro: 100-200 MB/s
- crypto-js JS puro: 30-80 MB/s
Para um ficheiro de 5 GB, a diferença entre Web Crypto e JS puro é ~3 segundos versus ~50 segundos. Perceptível pelo utilizador. Prefira sempre Web Crypto para o que suporta. Use bibliotecas WASM (libsodium.js, argon2-browser) apenas para algoritmos que a Web Crypto não tem (ChaCha20-Poly1305, Argon2id, X25519 em navegadores mais antigos).
Processamento em blocos para ficheiros grandes
Ficheiros com mais de ~500 MB não cabem confortavelmente num único ArrayBuffer na maioria dos dispositivos. Divida-os em blocos de 1-4 MB e encripte cada um.
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;
}
Porque 4 MB? Blocos mais pequenos (por exemplo, 64 KB) incorrem em sobrecarga por chamada da Web Crypto API que domina para inputs pequenos. Blocos maiores (por exemplo, 64 MB) não cabem bem em cache L2/L3 e têm maior pressão de memória. 1-4 MB tende a ser o ponto óptimo tanto em desktop como em dispositivos móveis.
Web Workers para manter a UI responsiva
A encriptação na thread principal bloqueia o rendering e o input. Para qualquer coisa que demore mais de ~500 ms, delegue para um 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) => { /* processar ct */ };
Transfira ArrayBuffers com o segundo argumento para postMessage — isso move a propriedade (sem cópia) em vez de clonar. Sem transferência, clonar um bloco de 4 MB adiciona ~20 ms de sobrecarga por bloco.
A Web Crypto API está disponível nos Workers, por isso a encriptação real corre ali com desempenho idêntico à thread principal.
Encriptação paralela de blocos
A encriptação AES-GCM por bloco é independente desde que os nonces não colidam. Derive nonces deterministicamente a partir do índice do bloco e pode encriptar blocos em paralelo em múltiplos Workers. Os retornos decrescentes surgem após ~4 workers em hardware típico porque a Web Crypto é tão rápida que o gargalo passa para a leitura de ficheiros e a passagem de mensagens entre threads. Meça antes de se comprometer com a complexidade; por vezes a encriptação sequencial de um único worker é tão rápida como a paralela de múltiplos devido à sobrecarga.
Streaming com Readable Streams
Para ficheiros verdadeiramente grandes (20+ GB), evite carregar até blocos em memória de uma vez. Use 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();
Isto mantém o uso de memória limitado a um bloco de cada vez. O navegador lê do disco, encripta, escreve para o stream de upload, lê o bloco seguinte. O uso de memória fica abaixo de 10 MB independentemente do tamanho do ficheiro.
Concorrência de upload
A encriptação corre em paralelo com o upload, não de forma serial. Enquanto o bloco N é enviado, o bloco N+1 está a ser encriptado. Use um pipeline:
const encryptQueue = [];
const uploadQueue = [];
const MAX_IN_FLIGHT = 4;
// O encriptador produz, o uploader consome
async function pipeline() {
// Inicia a encriptação dos primeiros blocos
for (let i = 0; i < MAX_IN_FLIGHT; i++) startEncrypt(i);
// À medida que cada um termina, inicia o upload e enfileira a próxima encriptação
// (fila limitada mantém o uso de memória previsível)
}
O slow-start do TCP e a sobrecarga de handshake TLS significam que os uploads iniciais são lentos. Manter 4-8 uploads concorrentes para uma única origem mantém o pipeline cheio sem atingir os limites do navegador (6 ligações por origem no Chrome/Firefox).
WebAssembly para primitivos em falta
Para a derivação de chaves Argon2id ou ChaCha20-Poly1305, a Web Crypto não tem suporte nativo. As bibliotecas WASM preenchem a lacuna:
- libsodium.js fornece Argon2id, XChaCha20-Poly1305,
crypto_secretstream - argon2-browser fornece apenas Argon2 mas com bundle menor
- @noble/hashes fornece Argon2 em JS puro (mais lento) com bundle pequeno
As versões WASM atingem 60-80% da velocidade nativa para a maioria das cargas criptográficas. Para transferência de ficheiros, uma derivação Argon2id de 1-2 segundos no upload e download é aceitável; uma derivação em JS puro de 5 segundos não é.
Carregue o WASM dinamicamente para não bloquear o render inicial da página:
const sodium = await import('libsodium-wrappers');
await sodium.ready;
Reporte de progresso
Encriptações grandes precisam de feedback de progresso ou os utilizadores assumem que a aplicação congelou. Conte os bytes encriptados e envie eventos de progresso:
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 });
}
Limite as actualizações da UI a ~10 Hz via requestAnimationFrame ou uma verificação simples de timestamp; actualizações mais frequentes desperdiçam ciclos em repintagens que os humanos não conseguem ver.
Tecto de memória e pressão do GC
Cada ArrayBuffer vive até não ter mais referências. Manter 20 blocos encriptados de 4 MB cada significa ~80 MB de memória fixada. Em navegadores móveis com limites de memória apertados (o iOS Safari limita a ~200-400 MB por tab), isto é relevante.
Liberte referências prontamente:
for (let i = 0; i < chunks.length; i++) {
const chunk = chunks[i];
chunks[i] = null; // deixar o GC recuperar
const ct = await encrypt(chunk);
await upload(ct);
}
Evite manter um array completo de blocos encriptados em memória; envie-os para o uploader em streaming e liberte as referências à medida que avança.
Metas reais
Para uma transferência de ficheiro encriptado de 1 GB num tab de navegador: tempo de encriptação 1-3 segundos (com aceleração hardware), tempo de upload 30 segundos numa ligação de 300 Mbps, total de cerca de 35 segundos em tempo de relógio (maioritariamente limitado pela rede), memória abaixo de 50 MB de pico com streaming adequado, UI responsiva durante todo o processo (thread principal nunca bloqueada mais de 50 ms). Quem falhar estes objectivos, os utilizadores notam. O limite de 10 GB do HexaTransfer é atingível no navegador porque a Web Crypto combinada com streaming em blocos mantém todo o caminho eficiente.
Experimente em hexatransfer.com — gratuito, sem conta, até 10 GB.
Envie arquivos grandes com segurança e criptografia de ponta a ponta
Transfira arquivos de até 10 GB gratuitamente com criptografia de ponta a ponta. Sem necessidade de conta. Seus arquivos são criptografados no navegador antes do envio — ninguém mais pode lê-los.
Enviar um arquivo