W3docs

Javascript Fetch: progreso de descarga

Aprende a rastrear el progreso de descarga con la Fetch API usando ReadableStream, Content-Length y barras de progreso en JavaScript.

La Fetch API es la forma moderna de realizar solicitudes de red en JavaScript, pero await fetch(...) se resuelve en cuanto llegan las cabeceras de la respuesta, mucho antes de que el cuerpo haya terminado de descargarse. Para mostrar una barra de progreso de un archivo grande necesitas leer el cuerpo de forma incremental y contar los bytes a medida que llegan. Este capítulo explica cómo hacerlo exactamente con un ReadableStream, por qué importa Content-Length, cómo construir una interfaz de progreso y lo único que fetch no puede hacer (progreso de subida).

Por qué fetch necesita un stream para el progreso

Cuando escribes const data = await response.json(), el navegador almacena en búfer el cuerpo completo internamente y solo lo devuelve cuando está completo: no existe ningún gancho para observar los bytes en tránsito. Sin embargo, el cuerpo de la Response está expuesto como ReadableStream a través de response.body. Extrayendo fragmentos de ese stream por tu cuenta puedes medir cuánto ha llegado y actualizar la interfaz en cada fragmento.

La receta es siempre la misma:

  1. Obtén un reader: const reader = response.body.getReader().
  2. Itera con reader.read(), que se resuelve en { done, value }value es un fragmento Uint8Array.
  3. Suma value.length a un total acumulado e informa del progreso.
  4. Conserva los fragmentos y reensámblalos una vez que done sea true.

Lectura del cuerpo como ReadableStream

Para conocer el porcentaje también necesitas el tamaño total. El servidor debería enviar una cabecera de respuesta Content-Length; léela con response.headers.get('Content-Length'). A menudo falta (transferencia por fragmentos, compresión gzip o un servidor que simplemente la omite), por lo que el ejemplo siguiente recurre a un tamaño proporcionado por el llamador.

javascript— editable

La función fetchWithProgress obtiene datos de una URL especificada y rastrea el progreso de la descarga. Lee el cuerpo de la respuesta en fragmentos mediante la interfaz ReadableStream y llama al callback onProgress con la longitud recibida y la longitud total del contenido. Los datos descargados se reconstruyen a partir de los fragmentos y se devuelven como una cadena decodificada.

Nota

La función intenta leer la cabecera Content-Length automáticamente. Si la cabecera falta (algo habitual con transferencias por fragmentos o compresión), recurre al parámetro fallbackSize. En producción, el desarrollador o el servidor deben proporcionar este tamaño de respaldo. Para archivos grandes, evita almacenar en búfer todos los fragmentos en memoria; procésalos de forma incremental o utiliza response.blob() para respuestas que no sean de texto.

Una vez que el bucle termina, tienes un array de fragmentos Uint8Array. Para convertirlos en datos utilizables, cópialos en un único búfer y decifícalos (para texto) o envuélvelos en un Blob (para archivos binarios, imágenes, descargas). Consulta JavaScript Blob para trabajar con datos binarios y desencadenar descargas de archivos.

Mostrar el progreso a los usuarios

Una barra de progreso es simplemente un elemento cuyo ancho sigue la relación received / total. Pasa un callback updateProgressBar en lugar del de registro:

<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>
Advertencia

Si estás descargando un archivo muy grande, evita actualizar la interfaz con demasiada frecuencia. Por ejemplo, en lugar de actualizar la barra de progreso en cada fragmento, puedes hacerlo con menos frecuencia (p. ej., cada pocos fragmentos o basándote en un intervalo de tiempo). Esto ayuda a mantener tu interfaz ligera.

Los elementos HTML del ejemplo anterior crean una barra de progreso para visualizar el progreso de la descarga y un área de texto preformateado para mostrar el contenido descargado. El div progress-bar actúa como contenedor, y el div progress representa el avance real de la descarga.

El código JavaScript actualiza la barra de progreso en función de la longitud de los datos recibidos y la longitud total del contenido. La función updateProgressBar calcula el porcentaje de datos descargados y ajusta el ancho de la barra de progreso en consecuencia. El listener de eventos activa la función fetchWithProgress al cargar la página, actualizando la barra de progreso y mostrando el contenido descargado en el elemento output.

Nota: fetchWithProgress está definida en el fragmento anterior. En un proyecto real, asegúrate de que esté en el ámbito adecuado (p. ej., mediante importaciones de módulos, agrupación o una etiqueta de script global).

Lo que fetch no puede hacer: progreso de subida

La técnica de stream solo rastrea el progreso de descarga (respuesta). A día de hoy no existe una forma estándar de observar el progreso de subida (cuerpo de la solicitud) con fetch — el cuerpo de la solicitud no está expuesto como un stream observable en los navegadores. Si necesitas una barra de progreso mientras envías un archivo, recurre a XMLHttpRequest, cuyo 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 indica si event.total es conocido; compruébalo siempre antes de calcular un porcentaje.

Cancelar una descarga en curso

Una descarga larga debería poder cancelarse. Pasa un AbortSignal a fetch y llama a controller.abort() para detener tanto la solicitud como el bucle del stream; consulta Fetch: Abort para ver el patrón completo.

Consejos profesionales

  • Maneja datos binarios: TextDecoder solo funciona para texto. Usa response.blob() o response.arrayBuffer() para archivos binarios.
  • Limita las actualizaciones de progreso: Las lecturas rápidas de fragmentos pueden bloquear el hilo de la interfaz. Aplica throttle o debounce al callback de progreso.
  • Gestión de memoria: Evita almacenar todos los fragmentos en un array para archivos grandes. Procésalos de forma incremental.
  • Respaldo de Content-Length: La cabecera suele faltar. Proporciona siempre un fallbackSize fiable.
  • Optimiza la retroalimentación al usuario: Proporcionar información en tiempo real sobre el progreso de la descarga mejora la satisfacción del usuario y puede hacer que tu aplicación se sienta más receptiva.
  • Aprovecha las capacidades del navegador: Los distintos navegadores pueden tener soporte variable para funciones avanzadas. Prueba tu implementación en varios navegadores para garantizar la compatibilidad.
  • Simplifica cuando sea posible: Para descargas en las que el progreso de streaming no sea estrictamente necesario, response.arrayBuffer() ofrece una forma más sencilla de reconstruir datos sin concatenar fragmentos manualmente.

Con estas ideas y ejemplos, ya estás preparado para implementar la Fetch API con seguimiento del progreso de descarga en tus proyectos, ofreciendo a los usuarios una experiencia fluida e informativa.

Conclusión

Dominar la Fetch API implica entender no solo cómo realizar solicitudes básicas, sino también cómo manejar escenarios más avanzados como el seguimiento del progreso de descarga. Usando ReadableStreams, podemos monitorizar y proporcionar información sobre las descargas, mejorando significativamente la experiencia del usuario. Implementar estas técnicas garantizará que tus aplicaciones sean robustas, fáciles de usar y capaces de gestionar grandes transferencias de datos de manera eficiente.

Práctica

Práctica
¿Cuáles son los pasos clave para rastrear el progreso de descarga usando la Fetch API?
¿Cuáles son los pasos clave para rastrear el progreso de descarga usando la Fetch API?
Was this page helpful?