loading...

Progress Indicator With Fetch

samthor profile image Sam Thorogood Updated on ・2 min read

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
}

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();
  }

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

And grab the reader, which lets us get chunks:

  const reader = response.body.getReader();

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;

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

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

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

Fin

20 πŸ‘‹

Posted on by:

samthor profile

Sam Thorogood

@samthor

Developer Relations for Web at Google.

Discussion

markdown guide
 

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
}
 

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.

 

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'.
 

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

 

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

 

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

 

I assume the at variable is something like?..

let at = 0;
 

Oh yeah, I'll fix that. Thanks!