57

Is there a way in JS to get the progress of a loading image while the image is being loaded? I want to use the new Progress tag of HTML5 to show the progress of loading images.

I wish there was something like:

var someImage = new Image()
someImage.onloadprogress = function(e) { progressBar.value = e.loaded / e.total };
someImage.src = "image.jpg";
Light
  • 1,647
  • 3
  • 22
  • 39
  • 7
    already looked at this? http://blogs.adobe.com/webplatform/2012/01/13/html5-image-progress-events/ – P1nGu1n Jan 08 '13 at 15:43
  • Possibile duplicate of http://stackoverflow.com/questions/2515142/is-it-possible-to-get-download-progress-of-video-image-in-html5 – freedev Jan 09 '13 at 22:15
  • Possible workaround: use a generic `...loading` indicator until `img.complete && img.naturalWidth > 0` – Ronnie Royston Jan 13 '18 at 19:24

7 Answers7

61

With this, you add 2 new functions on the Image() object:

 Image.prototype.load = function(url){
        var thisImg = this;
        var xmlHTTP = new XMLHttpRequest();
        xmlHTTP.open('GET', url,true);
        xmlHTTP.responseType = 'arraybuffer';
        xmlHTTP.onload = function(e) {
            var blob = new Blob([this.response]);
            thisImg.src = window.URL.createObjectURL(blob);
        };
        xmlHTTP.onprogress = function(e) {
            thisImg.completedPercentage = parseInt((e.loaded / e.total) * 100);
        };
        xmlHTTP.onloadstart = function() {
            thisImg.completedPercentage = 0;
        };
        xmlHTTP.send();
    };

    Image.prototype.completedPercentage = 0;

And here you use the load function and append the image on a div.

var img = new Image();
img.load("url");
document.getElementById("myDiv").appendChild(img);

During the loading state you can check the progress percentage using img.completedPercentage.

Sebastián Espinosa
  • 2,123
  • 13
  • 23
  • u can use the img object as src of a img tag. – Sebastián Espinosa Apr 15 '14 at 05:14
  • What does this line do? parseInt(thisImg.completedPercentage = (e.loaded / e.total) * 100); Is it written wrong? – Levent Esen Nov 06 '15 at 14:26
  • oh, it is, should work but its wrong, let me edit this – Sebastián Espinosa Nov 06 '15 at 19:03
  • This doesn't appear to work in *Safari* v10. It doesn't seem to actually add the new `load` function to the object prototype and `img.load()` is listed as `undefined`. – biscuitstack Sep 05 '17 at 11:51
  • @biscuitstack thats weird, its working for me in Safari 10.1.2, are you sure you are adding the prototype before creating the Image object? – Sebastián Espinosa Sep 06 '17 at 18:05
  • @SebastiánEspinosa I'm currently trying to figure it out here: https://stackoverflow.com/questions/46074053/function-undefined-on-prototype-of-builtin-existing-javascript-objects . It may actually be a bug in 10.2. At this point, I've no idea. – biscuitstack Sep 06 '17 at 18:09
  • @biscuitstack did you ever get to the bottom of the Safari 10.2 mystery? If so, could you drop a note here to describe your findings? – duhaime Apr 30 '18 at 23:48
  • 3
    @duhaime I did. I get very frustrated with the myopic nature of SO at times as I was requested to delete my answer from my link above. It was said to be 'unlikely of use to anyone else'. I contested this but.. I digress. Anyway, the standard uBlock plugin filters for Safari were blocking `Image.prototype.load` from completing, whereas they're not in Chrome and Firefox. The fork developers have not acknowledged my bug report so I don't know if it will be resolved. If you're not using uBlock, disable all plugins and see if that helps isolate the cause. – biscuitstack May 01 '18 at 14:00
  • This does not allow the image to load progressively. – Vladimir Panteleev Apr 02 '22 at 22:02
19

Sebastian's answer is excellent, the best I've seen to this question. There are, however, a few possible improvements. I use his code modified like this:

Image.prototype.load = function( url, callback ) {
    var thisImg = this,
        xmlHTTP = new XMLHttpRequest();

    thisImg.completedPercentage = 0;

    xmlHTTP.open( 'GET', url , true );
    xmlHTTP.responseType = 'arraybuffer';

    xmlHTTP.onload = function( e ) {
        var h = xmlHTTP.getAllResponseHeaders(),
            m = h.match( /^Content-Type\:\s*(.*?)$/mi ),
            mimeType = m[ 1 ] || 'image/png';
            // Remove your progress bar or whatever here. Load is done.

        var blob = new Blob( [ this.response ], { type: mimeType } );
        thisImg.src = window.URL.createObjectURL( blob );
        if ( callback ) callback( this );
    };

    xmlHTTP.onprogress = function( e ) {
        if ( e.lengthComputable )
            thisImg.completedPercentage = parseInt( ( e.loaded / e.total ) * 100 );
        // Update your progress bar here. Make sure to check if the progress value
        // has changed to avoid spamming the DOM.
        // Something like: 
        // if ( prevValue != thisImage completedPercentage ) display_progress();
    };

    xmlHTTP.onloadstart = function() {
        // Display your progress bar here, starting at 0
        thisImg.completedPercentage = 0;
    };

    xmlHTTP.onloadend = function() {
        // You can also remove your progress bar here, if you like.
        thisImg.completedPercentage = 100;
    }

    xmlHTTP.send();
};

Mainly I added a mime-type and some minor details. Use as Sebastian describes. Works well.

Julian Jensen
  • 321
  • 2
  • 5
  • 2
    Do you know which browsers would support this? And whether mobile? The answer below says XHR solution is not supported by all browsers… – Nik Sumeiko Jul 20 '14 at 20:12
  • Agree @manakor would be good to see the browser support mentioned in this answer – Bill Mar 30 '15 at 22:27
  • 1
    @manakor: I find http://caniuse.com invaluable for questions like yours. – Michael Scheper Jul 20 '16 at 09:05
  • You should not do your callback on `xmlHTTP.onload` because image properties like `naturalWidth` and `naturalHeight` are not available yet. You should do a separate `thisImg.onload = function() {if ( callback ) callback();};` instead. – TheStoryCoder Feb 14 '17 at 22:40
  • One downside with using `XMLHttpRequest` is that the Origin header is not set on the request - but it will be it you just set the `src` attribute on the `Image` object directly (at least in Chrome) – TheStoryCoder Feb 14 '17 at 23:21
18

Just to add to the improvements, I've modified Julian's answer (which in turn modified Sebastian's). I've moved the logic to a function instead of modifying the Image object. This function returns a Promise that resolves with the URL object, which only needs to be inserted as the src attribute of an image tag.

/**
 * Loads an image with progress callback.
 *
 * The `onprogress` callback will be called by XMLHttpRequest's onprogress
 * event, and will receive the loading progress ratio as an whole number.
 * However, if it's not possible to compute the progress ratio, `onprogress`
 * will be called only once passing -1 as progress value. This is useful to,
 * for example, change the progress animation to an undefined animation.
 *
 * @param  {string}   imageUrl   The image to load
 * @param  {Function} onprogress
 * @return {Promise}
 */
function loadImage(imageUrl, onprogress) {
  return new Promise((resolve, reject) => {
    var xhr = new XMLHttpRequest();
    var notifiedNotComputable = false;

    xhr.open('GET', imageUrl, true);
    xhr.responseType = 'arraybuffer';

    xhr.onprogress = function(ev) {
      if (ev.lengthComputable) {
        onprogress(parseInt((ev.loaded / ev.total) * 100));
      } else {
        if (!notifiedNotComputable) {
          notifiedNotComputable = true;
          onprogress(-1);
        }
      }
    }

    xhr.onloadend = function() {
      if (!xhr.status.toString().match(/^2/)) {
        reject(xhr);
      } else {
        if (!notifiedNotComputable) {
          onprogress(100);
        }

        var options = {}
        var headers = xhr.getAllResponseHeaders();
        var m = headers.match(/^Content-Type\:\s*(.*?)$/mi);

        if (m && m[1]) {
          options.type = m[1];
        }

        var blob = new Blob([this.response], options);

        resolve(window.URL.createObjectURL(blob));
      }
    }

    xhr.send();
  });
}

/*****************
 * Example usage
 */

var imgContainer = document.getElementById('imgcont');
var progressBar = document.getElementById('progress');
var imageUrl = 'https://placekitten.com/g/2000/2000';

loadImage(imageUrl, (ratio) => {
  if (ratio == -1) {
    // Ratio not computable. Let's make this bar an undefined one.
    // Remember that since ratio isn't computable, calling this function
    // makes no further sense, so it won't be called again.
    progressBar.removeAttribute('value');
  } else {
    // We have progress ratio; update the bar.
    progressBar.value = ratio;
  }
})
.then(imgSrc => {
  // Loading successfuly complete; set the image and probably do other stuff.
  imgContainer.src = imgSrc;
}, xhr => {
  // An error occured. We have the XHR object to see what happened.
});
<progress id="progress" value="0" max="100" style="width: 100%;"></progress>

<img id="imgcont" />
Parziphal
  • 6,222
  • 4
  • 34
  • 36
  • Could this be transformed into a snippet? – Zze Feb 13 '17 at 04:42
  • (honestly I don't know if you can even XMLHttpRequest in SO - but that would make this a great answer compared to the rest) – Zze Feb 13 '17 at 05:02
  • Well, it's working. If the image gets cached by your browser, try checking "Disable cache" on your Dev Tools (and use throttling if it loads too fast). – Parziphal Feb 13 '17 at 05:07
  • Thanks~ I realized that I could pass `-1` instead of `false` if progress isn't computable. That will keep the parameter always a number. I'll make the edit. – Parziphal Feb 13 '17 at 05:13
  • Beautiful answer! – krulik Sep 30 '20 at 11:33
5

Actually, in latest chrome you can use it.

$progress = document.querySelector('#progress');

var url = 'https://placekitten.com/g/2000/2000';

var request = new XMLHttpRequest();
request.onprogress = onProgress;
request.onload = onComplete;
request.onerror = onError;

function onProgress(event) {
  if (!event.lengthComputable) {
    return;
  }
  var loaded = event.loaded;
  var total = event.total;
  var progress = (loaded / total).toFixed(2);

  $progress.textContent = 'Loading... ' + parseInt(progress * 100) + ' %';

  console.log(progress);
}

function onComplete(event) {
  var $img = document.createElement('img');
  $img.setAttribute('src', url);
  $progress.appendChild($img);
  console.log('complete', url);
}

function onError(event) {
  console.log('error');
}


$progress.addEventListener('click', function() {
  request.open('GET', url, true);
  request.overrideMimeType('text/plain; charset=x-user-defined');
  request.send(null);
});
<div id="progress">Click me to load</div>
Chris Panayotoff
  • 1,744
  • 21
  • 24
  • 4
    Христо: I suggest you define what 'the latest' and 'it' are. You might get more upvotes that way, too. – Michael Scheper Jul 20 '16 at 09:07
  • Is this compatible to all the browsers ? – ideeps Jan 05 '18 at 03:05
  • This is the best answer IMO. How it works: Send an XHR request for the resource, and monitor the progress. Once the request completes, you can use the cached resource in the browser to create the image, and it should be ready immediately. – Brian Hannay Dec 26 '20 at 22:49
  • The problem with each of these answers is that the image doesn't show up until it's fully loaded. Which might be not desirable for gif animations or progressive jpegs. It also won't work if browser cache is disabled. – vanowm May 14 '23 at 22:33
1

for xmlhttpreq v2 check, use:

var xmlHTTP = new XMLHttpRequest();
if ('onprogress' in xmlHTTP) {
 // supported 
} else {
 // isn't supported
}
biscuitstack
  • 11,591
  • 1
  • 26
  • 41
Pavel Zheliba
  • 89
  • 1
  • 6
1

Here is a small update of the code of Julian Jensen in order to be able to draw the image in a Canvas after it is loaded :

xmlHTTP.onload = function( e ) {
        var h = xmlHTTP.getAllResponseHeaders(),
            m = h.match( /^Content-Type\:\s*(.*?)$/mi ),
            mimeType = m[ 1 ] || 'image/png';
            // Remove your progress bar or whatever here. Load is done.

        var blob = new Blob( [ this.response ], { type: mimeType } );
        thisImg.src = window.URL.createObjectURL( blob );

         thisImg.onload = function()
            {
                if ( callback ) callback( this );
            };
    };
kamel B
  • 303
  • 1
  • 3
  • 9
0

If you want to process your loaded image, than you have to add one more function, because

thisImg.src = window.URL.createObjectURL(blob)

just starts to process the image as a thread.

You have to add a new a function to the body of load prototype, like

  this.onload = function(e)
  {
    var canvas = document.createElement('canvas')

    canvas.width = this.width
    canvas.height = this.height

    canvas.getContext('2d').drawImage(this, 0, 0)
   }

This make me headache to realize :)