Check out "Do you speak JavaScript?" - my latest video course on advanced JavaScript.
Language APIs, Popular Concepts, Design Patterns, Advanced Techniques In the Browser

Write your own progress indicator with vanilla JavaScript

Write your own progress indicator with vanilla JavaScript Photo by Lindsay Henwood

Many moons ago, I was writing a lot of Flash applications. One thing was typical, and that was a progress indication of something that we were downloading. The loaded bytes of the Flash file (.swf) itself and later of some other resource the application needs. Once I started writing everything in JavaScript, this feature disappeared. In this post, we'll see how to implement it with vanilla JavaScript.

Using the fetch API

We need our resources to be streamed to us to implement an indicator. I recently learned about the browser support of streams API. I didn't know I could use streams with the fetch API. And that's because this approach only works with some web servers and not every resource. If the web server is not streaming the file, the promise returned by the fetch function is resolved once everything is downloaded. However, if the server is streaming, the body in the response is a proper ReadableStream.

Before seeing what the client JavaScript looks like, let's see a simple Node.js server that streams a file called video.webm.

const http = require('http');
const fs = require('fs');
const path = require('path');

const filePath = path.join(__dirname, 'video.webm');

function serveFile(req, res) {
  const stat = fs.statSync(filePath);
  res.writeHead(200, {
    'Content-Type': 'video/webm',
    'Content-Length': stat.size,
  });
  const readStream = fs.createReadStream(filePath);
  readStream.pipe(res);
}

const server = http.createServer(serveFile);
const port = 3000;
server.listen(port, () => {
  console.log(`Server is running on port ${port}`);
});

Notice that we are setting proper Content-Length headers based on the video's file size.

The JavaScript that we need in the browser starts with a regular fetch call, but then we create a stream reader by using the getReader method of the ReadableStream.

const url = '/video.webm';
const root = document.querySelector('#root');
const response = await fetch(url);
const contentLength = response.headers.get('Content-Length');
const total = parseInt(contentLength, 10);
let loaded = 0;
const reader = response.body.getReader();

(async function read() {
  const { done, value } = await reader.read();
  if (done) {
    root.innerHTML = 'done';
    return;
  }
  loaded += value.length;
  const percent = (loaded / total) * 100;
  root.innerHTML = Math.ceil(percent) + '% downloaded';
  read();
})();

We are using recursion to read the content chunk by chunk. At every chunk, we can see the progress and calculate the percentage of the loaded data.

What if there is no streaming

There is still a way to implement a progress indicator even if the web server is not streaming the resource. This is possible via the "low level" XMLHttpRequest API. This API was the primary way of making http/s requests back in the day. Today, all browsers support the fetch API, so we no longer rely on XHR. However, it can still be helpful for our use case here.

const url = '/video.webm';
const root = document.querySelector('#root');
const xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.responseType = 'blob';
xhr.onprogress = (event) => {
  if (event.lengthComputable) {
    const percent = ((event.loaded / event.total) * 100).toFixed(2);
    root.innerHTML = Math.ceil(percent) + '% downloaded';
  }
};
xhr.onload = () => {
  if (xhr.status === 200) {
    const blob = xhr.response;
    console.log(blob);
    root.innerHTML = 'done';
  }
};
xhr.send();

I hope you find this helpful.

P.S. Here's a list of interesting progress bars - codepen.io/tag/progress-bar.

If you enjoy this post, share it on Twitter, Facebook or LinkedIn.