DEV Community

loading...

Progress Indicator With Fetch

Sam Thorogood
Developer Relations for Web at Google.
・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 πŸ‘‹

Discussion (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 Author

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 Author

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
AnthumChris

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 Author

Oh yeah, I'll fix that. Thanks!