DEV Community

Sam Thorogood
Sam Thorogood

Posted on • Edited on

Progress Indicator With Fetch

A quick tip: in a previous demo, I showed how we can download a large file to seed the content for a Service Worker. If you look fast enough, you'll see a progress indicator. (Although for a small file, blink and you'll miss it!) 👀

The code is pretty simple. Let's start with a simple async fetch:

async function downloadFile(url) {
  const response = await fetch(url);
  const arrayBuffer = await response.arrayBuffer();
  const bytes = new Uint8Array(arrayBuffer);
  // do something with bytes
}
Enter fullscreen mode Exit fullscreen mode

The arrayBuffer call waits until the entire target has downloaded before returning the bytes. Instead, we can consume 'chunks' of the file (since we'll get parts of the file over time) at a time, to get a sense of percentage.

Check The Header

Firstly, we read the "Content-Length" header of our response: this is something the server sends us before the data, so we can actually work out how far along we've gone:

  const response = await fetch(url);
  const length = response.headers.get('Content-Length');
  if (!length) {
    // something was wrong with response, just give up
    return await response.arrayBuffer();
  }
Enter fullscreen mode Exit fullscreen mode

If there's no valid header, then either there's something wrong with the response, or the server hasn't told us how long it is. You can just fall back to whatever you were doing before.

Chunks

Your browser is receiving chunks of bytes from the remote server as the data arrives. Since we know how long the total response will be, we can prepare a buffer for it:

  const array = new Uint8Array(length);
  let at = 0;  // to index into the array
Enter fullscreen mode Exit fullscreen mode

And grab the reader, which lets us get chunks:

  const reader = response.body.getReader();
Enter fullscreen mode Exit fullscreen mode

Now, we can store where we're up to (in at), and insert every new chunk into the output:

  for (;;) {
    const {done, value} = await reader.read();
    if (done) {
      break;
    }
    array.set(value, at);
    at += value.length;
  }
  return array;
Enter fullscreen mode Exit fullscreen mode

Within the loop above, we can log the progress as a percentage, something like:

    progress.textContent = `${(at / length).toFixed(2)}%`;
Enter fullscreen mode Exit fullscreen mode

Then as above, just return the array: we're done.

Fin

20 👋

Top comments (8)

Collapse
 
trezy profile image
Trezy • Edited

Nice! I've been wondering lately about checking the progress of a fetch request. However, you use a for loop without any arguments in your example:

for (;;) { '...' }

Why not use a while loop?

let downloadIsDone = false

while (!downloadIsDone) {
  const { done, value } = await reader.read()

  downloadIsDone = done
}
Collapse
 
samthor profile image
Sam Thorogood

It's honestly just preference! If you want to be pedantic, then my way is less work: the while (...) loop checks downloadIsDone even though we know it's false already, but this is splitting hairs.

FWIW, I write a lot of Go, which has a "naked" for loop: for { ... }, which I quite like—it's just the same as using (;;) in the body of a JS for loop.

Collapse
 
ahmednrana profile image
Rana Ahmed

Doesnt seem to be working with node-fetch
let response = await fetch('api.github.com/repos/javascripttut...);

const reader = response.body.getReader();

This gives error -> Property 'getReader' does not exist on type 'ReadableStream'.
Collapse
 
samthor profile image
Sam Thorogood

I don't believe node-fetch supports this. Read more.

Collapse
 
mitar profile image
Mitar

This does not work well when content-encoding is in effect. content-length does not match then the length of the contents streamed.

Collapse
 
anthumchris profile image
Chris

This can be solved with a server-side assist. Here's a performant x-file-size header example with Nginx:

github.com/AnthumChris/fetch-progr...

Collapse
 
koresar profile image
Vasyl Boroviak

I assume the at variable is something like?..

let at = 0;
Collapse
 
samthor profile image
Sam Thorogood

Oh yeah, I'll fix that. Thanks!