Let’s say you’re writing a web app, and of course you’re using JavaScript. So far, so good!
But then let’s say you need to load some images on the fly, and then do something useful AFTER each image is fully loaded.
IOW, maybe you want to adjust the layout of the image modal that pops up, or you want to do something crazy with a caption, or whatever…
Naturally, you’d use JavaScript to do this – including changing CSS on the fly.
So the question is: How do you use JavaScript to detect when an image is fully rendered on the page – not just loaded?
First thing’s first
When you have an IMG tag with a src attribute, you can just set the src to whatever image URL you want.
You might just think that you can then check document.getElementById(‘myImage’).complete, and POOF! You know the image is loaded, right?
WRONG!
The .complete attribute doesn’t mean the image is actually displayed on the page. All it really means is the following:
- Neither the
src
nor thesrcset
attribute is specified. - The
srcset
attribute is absent and thesrc
attribute, while specified, is the empty string (""
). - The image resource has been fully fetched and has been queued for rendering/compositing.
- The image element has previously determined that the image is fully available and ready for use.
- The image is “broken;” that is, the image failed to load due to an error or because image loading is disabled.
The key points for us are the third and fifth ones: Either the image has been fetched and is ready to be rendered (but has not been rendered yet), or your IMG url is broken.
So, you need to check two things to determine that an image is loaded and rendered:
- Check that the .complete attribute is true
- Check that the .naturalWidth and .naturalHeight attributes are both greater than 0
Note that before your image is actually rendered, .naturalWidth and .naturalHeight both return 0 – not null or undefined.
Let’s get coding!
So, off we go. First, you need some HTML, like so:
<img id="myImage" src="#" alt="My Image">
Then you need some JS:
var completeInterval = null, renderedInterval = null, count = 0; var theImg = document.getElementById('myImage'); theImg.src = '/images/some_new_image.jpg'; // Wait for image to be loaded (but not necessarily rendered) completeInterval = setInterval(function() { ++count; if (theImg.complete || count > 20) { // Cancel checking once IMG is loaded OR we've tried for ~9s already clearInterval(completeInterval); completeInterval = null; count = 0; if (count > 20) { // Image load went wrong, so do something useful: console.error("ERROR: Could not load image"); } else { // IMG is now 'complete' - but that just means it's in the render queue... // Wait for naturalWidth and naturalHeight to be > 0 since this shows // that the image is done being RENDERED (not just 'loaded/complete') renderedInterval = setInterval(function() { if (theImg.naturalHeight > 0 && theImg.naturalWidth > 0) { clearInterval(renderedInterval); renderedInterval = null; // Do whatever now that image is rendered (aka DONE): console.log("Image loaded - YAY!") } }, 100); } } }, 450);
All you’re doing is setting 2 intervals. The first interval, completeInterval, checks that .complete == true or that count > 20. Since this first interval runs every 450ms, 20 * 450ms = 9s. If the IMG can’t be loaded in 9s, we abort and do something like display an error message.
Assuming that .complete == true, then the first interval is cleared and the 2nd interval is triggered.
The second interval, renderedInterval, now checks every 100ms for .naturalWidth and .naturalHeight to be greater than 0. Once they are, we can clear renderedInterval and then do our “Yay the image has loaded and rendered!” code.
Pretty straightforward!
Note that it’s usually a good idea to set interval variables to null after calling clearInterval(). This may ensure that JavaScript’s garbage collection will free the memory associated with the old intervals a bit sooner.
The other thing is that there’s really no need to use promises. I know they’re The Current Thing, but that doesn’t mean they’re always the BEST thing. For a single image, you could do something like:
async function loadImage(imageUrl) { let img; const loadImagePromise = new Promise(resolve => { img = new Image(); img.onload = resolve; img.src = imageUrl; }); await loadImagePromise; console.log("Image loaded - YAY!"); return img; }
In the event that you need to wait for multiple images to load, it can seem a bit easier since you can create 1 promise for each image, and then do:
await Promise.all(promiseArray);
Generally speaking though, I find that Promise.all is never quite as useful as I think it will be. In fact, even in the promise-heavy code I’ve written for both Node.js and vanilla web JS, I’ve never used Promise.all. I usually need to have very fine-grained control of what my code is doing, and therefore I don’t mind loops and intervals. JavaScript is inherently asynchronous, anyway. Plus, using promises incorrectly often leads to massive headaches. It’s better to get down to the nitty gritty details and really understand what your code is doing, if it’s efficient, if it minimizes memory usage, and so on.
BTW, if you’re looking for a handy JavaScript library to avoid plain vanilla JS, check out my jQuery alternative PikaJS. It’s small, fast, and all I ever use now for all my projects!
Recent Comments