Download Progress with JavaScript’s Fetch Function

OpenJavaScript 0

Last updated: January 6, 2023.

You can track download progress using JavaScript’s fetch function by reading the body of the response object you receive from a request.

By reading this as a stream, you can access information about how many bytes have been downloaded so far. With this information, you can update a download progress indicator (text or progress bar)

As bytes are downloaded, you pass these into a new ReadableStream object on a Response object. This can then be read in the using the regular fetch methods (response.json(), response.blob(), response.text()) after the download is complete.

fetch('https://picsum.photos/400/400')
  .then(response => {

    const contentLength = response.headers.get('content-length');
    // Gets length in bytes (must be provided by server)

    let loaded = 0;
    // Will be used to track loading

    return new Response(

      new ReadableStream({
      // Creates new readable stream on the new response object
        
        start(controller) {
        // Controller has methods on that allow the new stream to be constructed

          const reader = response.body.getReader();
          // Creates a new reader to read the body of the fetched resources

          read();
          // Fires function below that starts reading

          function read() {

            reader.read()
            .then((progressEvent) => {
            // Starts reading, when there is progress this function will fire
              
              if (progressEvent.done) {
                controller.close();
                return; 
                // Will finish constructing new stream if reading fetched of resource is complete
              }
              
              loaded += progressEvent.value.byteLength;
              // Increase value of 'loaded' by latest reading of fetched resource

              console.log(Math.round(loaded/contentLength*100)+'%');
              // Displays progress via console log as %

              controller.enqueue(progressEvent.value);
              // Add newly read data to the new readable stream

              read();
              // Runs function again to continue reading and creating new stream

            })
          }
        }
      })
    );
  })
  .then(response => response.blob()) // Read new readable stream to blob
  .then(blob => {
    new Image().src = URL.createObjectURL(blob);
    document.body.appendChild(img);
    // Create new URL to blob image, set as src of image and append to DOM
  })

And below is an example of using the data being read from a fetched resource to update a CSS progress bar.

The bar advances by updating the width of the progress-bar-fill. This progressively spans the width of its parent element, progress-bar:

<style>

  body {
    font-family: Arial, Helvetica, sans-serif;
  }

  /* CSS Progress bar */
  .wrapper {
      width: 600px;
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      margin: 0px auto;
      margin-top: 30vh;
  }
  .progress-bar {
      width: 100%;
      background-color: whitesmoke;
      padding: 2px;
  }
  .progress-bar-fill {
      display: block;
      height: 40px;
      background-color: #65ef88;
      border-radius: 3px;
      width: 0%; /* Starts at 0%, updates with download progress */
          
      transition: width 30ms ease-in-out;
  }
  .progress-text {
      font-size: 1.5rem;
      padding: 10px;
  }
  img {
      display: block;
      margin: 0px auto;
  }
  
</style>
<body>

  <div class="wrapper">
    <div class="progress-bar">
      <span class="progress-bar-fill"></span>  
    </div>
    <span class="progress-text">Download starting...</span>
  </div>

</body>
<script>
    
  const fill = document.querySelector('.progress-bar-fill');
  const text = document.querySelector('.progress-text');

  fetch('https://picsum.photos/400/400')
  .then(response => {
    const contentLength = response.headers.get('content-length');
    let loaded = 0;
    return new Response(
      new ReadableStream({
        start(controller) {
          const reader = response.body.getReader();
          read();
          function read() {
            reader.read()
            .then((progressEvent) => {
              if (progressEvent.done) {
                controller.close();
                return; 
              }
              loaded += progressEvent.value.byteLength;

              const percentageComplete = Math.round(loaded/contentLength*100)+'%';
              fill.style.width = percentageComplete;
              text.innerText = percentageComplete;
              // Calculates % complete and updates progress bar and print text to DOM

              controller.enqueue(progressEvent.value);
              read();
            })
          }
        }
      })
    );
  })
  .then(response => response.blob())
  .then(blob => {
    const img = new Image()
    img.src = URL.createObjectURL(blob);
    document.body.appendChild(img);
  })

</script>

Related posts