54

Having read other people's questions I thought

window.onload=...

would answer my question. I have tried this but it executes the code the instant the page loads (not after the images load).

If it makes any difference the images are coming from a CDN and are not relative.

Anyone know a solution? (I'm not using jQuery)

John Wheal
  • 9,908
  • 6
  • 29
  • 39
  • 1
    What do you want to accomplish? Maybe there is something more efficient than preloading images. – Alp Jun 17 '12 at 12:36

10 Answers10

87

Want a one-liner?

Promise.all(Array.from(document.images).filter(img => !img.complete).map(img => new Promise(resolve => { img.onload = img.onerror = resolve; }))).then(() => {
    console.log('images finished loading');
});

Pretty backwards-compatible, works even in Firefox 52 and Chrome 49 (Windows XP era). Not in IE11, though.

Replace document.images with e.g. document.querySelectorAll(...) if you want to narrow the image list.

It uses onload and onerror for brevity. This might conflict with other code on the page if these handlers of the img elements are also set elsewhere (unlikely, but anyway). If you're not sure that your page doesn't use them and want to be safe, replace the part img.onload = img.onerror = resolve; with a lengthier one: img.addEventListener('load', resolve); img.addEventListener('error', resolve);.

It also doesn't test whether all images have loaded successfully (that there are no broken images). If you need this, here's some more advanced code:

Promise.all(Array.from(document.images).map(img => {
    if (img.complete)
        return Promise.resolve(img.naturalHeight !== 0);
    return new Promise(resolve => {
        img.addEventListener('load', () => resolve(true));
        img.addEventListener('error', () => resolve(false));
    });
})).then(results => {
    if (results.every(res => res))
        console.log('all images loaded successfully');
    else
        console.log('some images failed to load, all finished loading');
});

It waits until all images are either loaded or failed to load.

If you want to fail early, with the first broken image:

Promise.all(Array.from(document.images).map(img => {
    if (img.complete)
        if (img.naturalHeight !== 0)
            return Promise.resolve();
        else
            return Promise.reject(img);
    return new Promise((resolve, reject) => {
        img.addEventListener('load', resolve);
        img.addEventListener('error', () => reject(img));
    });
})).then(() => {
    console.log('all images loaded successfully');
}, badImg => {
    console.log('some image failed to load, others may still be loading');
    console.log('first broken image:', badImg);
});

Two latest code blocks use naturalHeight to detect broken images among the already loaded ones. This method generally works, but has some drawbacks: it is said to not work when the image URL is set via CSS content property and when the image is an SVG that doesn't have its dimensions specified. If this is the case, you'll have to refactor your code so that you set up the event handlers before the images begin to load. This can be done by specifying onload and onerror right in the HTML or by creating the img elements in the JavaScript. Another way would be to set src as data-src in the HTML and perform img.src = img.dataset.src after attaching the handlers.

user
  • 23,260
  • 9
  • 113
  • 101
  • 6
    Excellent answer. I found one broken edge case: On Firefox, it is possible for an `` to be in `completed===false` state indefinitely, when the image is never even attempted to be loaded because the protocol is unknown. For example, ``. Your code stills works because `onerror` is triggered, but it fails when you run the code dynamically. I made a demo [here](http://jsfiddle.net/f9ku7jn4/). Maybe a Firefox bug? – phil294 Oct 24 '20 at 13:23
  • @phil294, sounds like a bug indeed, should be reported to [Bugzilla](https://bugzilla.mozilla.org/). – user Nov 28 '20 at 03:55
  • 1
    Best answer here. A way to support background images would be a plus. – BenMorel Dec 11 '20 at 16:30
  • could you please explain why `img.onload = img.onerror = resolve` seems to work in the same way as `if( img.onload && img.onerror) resolve()`? – user3746571 Jul 02 '22 at 16:56
71

Here is a quick hack for modern browsers:

var imgs = document.images,
    len = imgs.length,
    counter = 0;

[].forEach.call( imgs, function( img ) {
    if(img.complete)
      incrementCounter();
    else
      img.addEventListener( 'load', incrementCounter, false );
} );

function incrementCounter() {
    counter++;
    if ( counter === len ) {
        console.log( 'All images loaded!' );
    }
}

Once all the images are loaded, your console will show "All images loaded!".

What this code does:

  • Load all the images in a variable from the document
  • Loop through these images
  • Add a listener for the "load" event on each of these images to run the incrementCounter function
  • The incrementCounter will increment the counter
  • If the counter has reached the length of images, that means they're all loaded

Having this code in a cross-browser way wouldn't be so hard, it's just cleaner like this.

Jens
  • 1,599
  • 14
  • 33
Florian Margaine
  • 58,730
  • 15
  • 91
  • 116
  • 6
    Two notes: If your script loads late, you may miss some events causing the script never to complete. Also, your browser may not load all images that are initially hidden by css. – Micros Jul 04 '17 at 15:13
  • @Micros okay, but how is your solution for that problem / question? – The Bndr Jun 27 '18 at 12:29
  • 1
    @TheBndr, just some things to keep in mind when implementing the answer above. – Micros Aug 13 '18 at 15:27
  • 1
    @Micros I have added a fix for your first issue – Jens Nov 20 '19 at 15:09
  • 1
    This just won't fire if some image is broken. You should also handle the `error` event. – user Mar 31 '20 at 11:52
  • @user: What's your recommendation for handling broken images? In my case, I was monitoring the number of images loaded and displaying a progress preloader accordingly. When all images are loaded, I hide the preloader and animate in the main content. I'm not sure what I should do if one of them is broken. – bytrangle Sep 10 '21 at 11:23
  • is it working correct for cached images? (each non-first page request) – Ilya Jul 18 '23 at 10:13
18

Promise Pattern will solve this problem in a best possible manner i have reffered to when.js a open source library to solve the problem of all image loading

function loadImage (src) {
    var deferred = when.defer(),
        img = document.createElement('img');
    img.onload = function () { 
        deferred.resolve(img); 
    };
    img.onerror = function () { 
        deferred.reject(new Error('Image not found: ' + src));
    };
    img.src = src;

    // Return only the promise, so that the caller cannot
    // resolve, reject, or otherwise muck with the original deferred.
    return deferred.promise;
}

function loadImages(srcs) {
    // srcs = array of image src urls

    // Array to hold deferred for each image being loaded
    var deferreds = [];

    // Call loadImage for each src, and push the returned deferred
    // onto the deferreds array
    for(var i = 0, len = srcs.length; i < len; i++) {
        deferreds.push(loadImage(srcs[i]));

        // NOTE: We could push only the promise, but since this array never
        // leaves the loadImages function, it's ok to push the whole
        // deferred.  No one can gain access to them.
        // However, if this array were exposed (e.g. via return value),
        // it would be better to push only the promise.
    }

    // Return a new promise that will resolve only when all the
    // promises in deferreds have resolved.
    // NOTE: when.all returns only a promise, not a deferred, so
    // this is safe to expose to the caller.
    return when.all(deferreds);
}

loadImages(imageSrcArray).then(
    function gotEm(imageArray) {
        doFancyStuffWithImages(imageArray);
        return imageArray.length;
    },
    function doh(err) {
        handleError(err);
    }
).then(
    function shout (count) {
        // This will happen after gotEm() and count is the value
        // returned by gotEm()
        alert('see my new ' + count + ' images?');
    }
);
Ajay Beniwal
  • 18,857
  • 9
  • 81
  • 99
2

Using window.onload will not work because it fires once the page is loaded, however images are not included in this definition of loaded.

The general solution to this is the ImagesLoaded jQuery plugin.

If you're keen on not using jQuery at all, you could at least try converting this plugin into pure Javascript. At 93 significant lines of code and with good commenting, it shouldn't be a hard task to accomplish.

1

You can have the onload event on the image that can callback a function that does the processing... Regarding how to handle if all images are loaded, I am not sure if any of the following mechanisms will work:

have a function that counts the number of images for which onload is called, if this is equal to the total number of images on your page then do your necessary processing.

Baz1nga
  • 15,485
  • 3
  • 35
  • 61
0
 <title>Pre Loading...</title>
 </head>

 <style type="text/css" media="screen"> html, body{ margin:0;
 padding:0; overflow:auto; }
 #loading{ position:fixed; width:100%; height:100%; position:absolute; z-index:1; ackground:white url(loader.gif) no-repeat center; }**
 </style>

 <script> function loaded(){
 document.getElementById("loading").style.visibility = "hidden"; }
 </script>

 <body onload="loaded();"> <div id="loading"></div>

 <img id="img" src="avatar8.jpg" title="AVATAR" alt="Picture of Avatar
 movie" />


 </body>
Donovant
  • 3,091
  • 8
  • 40
  • 68
0

I was looking for something like this, if you won't mind using setInterval this code is easy and straightforward. In my case I'm okay to use setInterval because it will run maybe 4-5 times.

const interval = setInterval(() => {
    const allImagesLoaded = [...document.querySelectorAll('img')]
      .map(x => x.complete)
      .indexOf(false) === -1;
    if (allImagesLoaded) {
      window.print();
      clearInterval(interval);
    }
  }, 500);
0

This is a slight variation of Florian Margaine's approach from above. This takes into account a possible broken link in one of the images. If you still need to use the images (e.g. using their height to calculate another element's height on load), this allows you to do that.

let imgs = document.querySelectorAll("img"),
counter = 0;

// Loop through images/check if it's loaded/if it is, increment the counter/else listen for when it does load and increment then
imgs.forEach((img) => (img.complete ? incrementCounter() : img.addEventListener("load", incrementCounter, false)));

function incrementCounter() {
    counter++;
    // If at least 1 image is loaded, do something
    if (counter !== 0) {
        calculateHeight();
    }
}
-2

I was about to suggest the same thing Baz1nga said.

Also, another possible option that's maybe not as foolproof but easier to maintain is to pick the most important/biggest image and attach an onload event to only that one. the advantage here is that there's less code to change if you later add more images to your page.

PPac
  • 1
-4

This works great:

$(function() {
 $(window).bind("load", function() {
    // code here
 });
});
Siong Thye Goh
  • 3,518
  • 10
  • 23
  • 31