Javascript Fetch: progresso de download
Aprenda a rastrear o progresso de download com a Fetch API usando ReadableStream, Content-Length e barras de progresso em JavaScript.
A Fetch API é a forma moderna de fazer requisições de rede em JavaScript, mas await fetch(...) é resolvido assim que os cabeçalhos da resposta chegam — muito antes de o corpo ter terminado de ser baixado. Para exibir uma barra de progresso para um arquivo grande, você precisa ler o corpo de forma incremental e contar os bytes à medida que chegam. Este capítulo explica exatamente como fazer isso com um ReadableStream, por que o Content-Length é importante, como construir uma UI de progresso e o que o fetch não consegue fazer (progresso de upload).
Por que o fetch precisa de um stream para o progresso
Quando você escreve const data = await response.json(), o navegador armazena em buffer o corpo inteiro internamente e só o retorna quando estiver completo — não há como observar os bytes durante a transferência. No entanto, o corpo da Response é exposto como um ReadableStream por meio de response.body. Ao extrair chunks desse stream você mesmo, pode medir quanto chegou e atualizar a UI a cada chunk.
A receita é sempre a mesma:
- Obtenha um leitor:
const reader = response.body.getReader(). - Execute um loop em
reader.read(), que é resolvido para{ done, value }—valueé um chunkUint8Array. - Adicione
value.lengtha um total acumulado e informe o progresso. - Guarde os chunks e remonte-os quando
donefortrue.
Lendo o corpo como um ReadableStream
Para saber a porcentagem, você também precisa do tamanho total. O servidor deve enviar um cabeçalho de resposta Content-Length; leia-o com response.headers.get('Content-Length'). Ele frequentemente está ausente (codificação de transferência em chunks, compressão gzip ou um servidor que simplesmente o omite), portanto o exemplo abaixo usa como fallback um tamanho fornecido pelo chamador.
A função fetchWithProgress busca dados de uma URL especificada e rastreia o progresso do download. Ela lê o corpo da resposta em chunks usando a interface ReadableStream e chama o callback onProgress com o comprimento recebido e o comprimento total do conteúdo. Os dados baixados são então reconstruídos a partir dos chunks e retornados como uma string decodificada.
A função tenta ler o cabeçalho Content-Length automaticamente. Se o cabeçalho estiver ausente (comum em transferências em chunks ou com compressão), ela usa o parâmetro fallbackSize como fallback. Em produção, o desenvolvedor ou o servidor deve fornecer esse tamanho de fallback. Para arquivos grandes, evite armazenar todos os chunks em memória; processe-os de forma incremental ou use response.blob() para respostas que não sejam texto.
Quando o loop terminar, você terá um array de chunks Uint8Array. Para transformá-los em dados utilizáveis, copie-os em um único buffer e decodifique (para texto) ou envolva-os em um Blob (para arquivos binários, imagens, downloads). Consulte JavaScript Blob para trabalhar com dados binários e disparar downloads de arquivos.
Exibindo o progresso para os usuários
Uma barra de progresso é apenas um elemento cuja largura acompanha received / total. Passe um callback updateProgressBar no lugar do de logging:
<body>
<div id="progress-bar" style="width: 100%; background-color: #e0e0e0;">
<div id="progress" style="width: 0; height: 20px; background-color: #76c7c0;"></div>
</div>
<div id="output"></div>
<script>
function updateProgressBar(received, total) {
const progressElement = document.getElementById('progress');
const percentage = Math.min(100, (received / total) * 100);
progressElement.style.width = percentage + '%';
}
document.addEventListener('DOMContentLoaded', () => {
const url = 'https://api.w3docs.com/uploads/media/default/0001/05/dd10c28a7052fb6d2ff13bc403842b797a73ff3b.txt';
const size = 3_900_000; // fallback size
// fetchWithProgress is defined in the previous code block
fetchWithProgress(url, updateProgressBar, size)
.then(data => {
document.getElementById('output').textContent = 'File content: ' + data.slice(0, 1000) + '...';
})
.catch(err => console.error("Download failed:", err));
});
</script>
</body>Se você estiver baixando um arquivo muito grande, evite atualizar a UI com frequência excessiva. Por exemplo, em vez de atualizar a barra de progresso a cada chunk individual, você pode atualizá-la com menos frequência (por exemplo, a cada alguns chunks ou com base em um intervalo de tempo). Isso ajuda a manter sua UI leve.
Os elementos HTML no exemplo acima configuram uma barra de progresso para visualizar o progresso do download e uma área de texto pré-formatada para exibir o conteúdo baixado. O div progress-bar serve como contêiner, e o div progress representa o progresso real do download.
O código JavaScript atualiza a barra de progresso com base no comprimento dos dados recebidos e no comprimento total do conteúdo. A função updateProgressBar calcula a porcentagem dos dados baixados e ajusta a largura da barra de progresso de acordo. O listener de evento dispara a função fetchWithProgress no carregamento da página, atualizando a barra de progresso e exibindo o conteúdo baixado no elemento output.
Observação: fetchWithProgress é definida no trecho anterior. Em um projeto real, certifique-se de que ela esteja no escopo (por exemplo, via importações de módulos, bundling ou uma tag de script global).
O que o fetch não consegue fazer: progresso de upload
A técnica de stream rastreia apenas o progresso de download (resposta). Até o momento, não há forma padrão de observar o progresso de upload (corpo da requisição) com fetch — o corpo da requisição não é exposto como um stream observável nos navegadores. Se você precisar de uma barra de progresso ao enviar um arquivo, recorra ao XMLHttpRequest, cujo objeto upload dispara eventos progress:
function uploadWithProgress(url, file, onProgress) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', url);
// upload.onprogress fires repeatedly while the body is sent
xhr.upload.onprogress = (event) => {
if (event.lengthComputable) {
const percent = Math.round((event.loaded / event.total) * 100);
onProgress(event.loaded, event.total, percent);
}
};
xhr.onload = () => resolve(xhr.responseText);
xhr.onerror = () => reject(new Error('Upload failed'));
xhr.send(file);
});
}event.lengthComputable informa se event.total é conhecido; sempre verifique esse valor antes de calcular uma porcentagem.
Cancelando um download em andamento
Um download longo deve ser cancelável. Passe um AbortSignal para o fetch e chame controller.abort() para interromper tanto a requisição quanto o loop do stream — consulte Fetch: Abort para o padrão completo.
Dicas profissionais
- Trate dados binários:
TextDecoderfunciona apenas para texto. Useresponse.blob()ouresponse.arrayBuffer()para arquivos binários. - Limite as atualizações de progresso: Leituras rápidas de chunks podem bloquear a thread da UI. Faça throttle ou debounce no callback de progresso.
- Gerenciamento de memória: Evite armazenar todos os chunks em um array para arquivos grandes. Processe-os de forma incremental.
- Fallback do Content-Length: O cabeçalho frequentemente está ausente. Sempre forneça um
fallbackSizeconfiável. - Otimize o feedback ao usuário: Fornecer feedback em tempo real sobre o progresso do download melhora a satisfação do usuário e pode tornar sua aplicação mais responsiva.
- Aproveite as capacidades do navegador: Navegadores diferentes podem ter suporte variado a recursos avançados. Teste sua implementação em múltiplos navegadores para garantir compatibilidade.
- Simplifique quando possível: Para downloads em que o progresso via streaming não é estritamente necessário,
response.arrayBuffer()oferece uma forma mais simples de reconstruir dados sem concatenar chunks manualmente.
Com esses insights e exemplos, você está agora preparado para implementar a Fetch API com rastreamento de progresso de download em seus projetos, oferecendo aos usuários uma experiência fluida e informativa.
Conclusão
Dominar a Fetch API envolve entender não apenas como fazer requisições básicas, mas também como lidar com cenários mais avançados, como rastrear o progresso de download. Ao usar ReadableStreams, podemos monitorar e fornecer feedback sobre downloads, melhorando significativamente a experiência do usuário. Implementar essas técnicas garantirá que suas aplicações sejam robustas, amigáveis ao usuário e capazes de lidar com grandes transferências de dados com eficiência.