Why is async function slowing down my image loading process? - javascript

I have written an image-loading function like this:
export function loadImage(url: string): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
if (image_map.has(url)) {
resolve(image_map.get(url));
} else {
let image: HTMLImageElement = new Image();
if (typeof window.static_url != 'undefined' && url[0] == '/') {
image.src = `${window.static_url}${url}`;
} else {
image.src = url;
}
image_map.set(url, image);
image.crossOrigin = 'Anonymous'; // Prevent canvas getImageData CORS issue
image.onload = function () {
resolve(image);
};
image.onerror = reject;
}
});
}
I have about 200 images to load, and prior to asking this question, I have been writing code like this:
for (let url of image_list) {
await loadImage(url);
}
And it took me ages (16s or more) to load all these images.
Today I decided to remove the await from my code, and miracle happened: the image loading process finished within 2s. How is that even possible? I thought javascript is single-threaded, and I had expected that, by removing await, the image loading should simply take place after the rest of my code, instead of taking place before, but the total time for them to load should not differ so greatly.

Your current code loads the images serially (one after the other).
You can instead execute them in parallel using promise all.
// 1. simultaneously kick off load image on each url
const promises = image_list.map(loadImage);
// 2. await them all
await Promise.all(promises);
// 3. Images have loaded.
Or more succinctly.
await Promise.all(image_list.map(loadImage));

Related

Wait for function to finish running before iterating

How can I wait for the image.onload function to finish running before iterating the next value in an array??
for (let i = 0; i < Object.keys(images_list_complex_form).length; i++) {
for (let k = 0; k < images_list_complex_form[i].length; k++) {
console.log('ITERATE');
image.onload = function () {
console.log('STARTED IMAGE ON LOAD');
}
}
}
I am receiving multiple 'iterates' before receiving started image on load
As sugguested by #amadan, i have tried adding in async and await into the for loop however, after doing so the image111.onload is never triggered anymore. I never get to see the console log "Started image on load"
async function rectifyMissingSavedImages(images_list_complex_form_array,annotationIndexArray){
console.log(images_list_complex_form_array);
for(let i = 0; i < Object.keys(images_list_complex_form_array).length; i++){
for(const[index2,value2] of Object.entries(images_list_complex_form_array[i])){
if (Object.keys(annotationIndexArray).length == 0 || (!annotationIndexArray[i] || annotationIndexArray[i].includes(String(k)) == false)) {
let image_url = URL.createObjectURL(value2);
let image111 = new Image();
let canvas = new fabric.Canvas();
console.log(image111);
await new Promise((resolve,reject) =>{
image111.onload = evt => {
console.log("??");
console.log('STARTED IMAGE ON LOAD');
let background = new fabric.Image(image);
canvas.setHeight(background.height);
canvas.setWidth(background.width);
canvas.setBackgroundImage(background, canvas.renderAll.bind(canvas));
console.log("ABOUT TO RESOLVE");
image.src = image_url;
resolve();
}
});
The easiest way is to create and await a promise inside the loop. Something like:
async function loadImages() {
for (...) {
await new Promise((resolve, reject) => {
image.onload = evt => {
console.log('STARTED IMAGE ON LOAD');
resolve();
}
image.onerror = reject;
});
}
}
This is the only way that a loop can "pause" in JavaScript. Any other way involves either chaining promises, which is messy, or nesting callbacks, which is even worse.
That task is not completely trivial. At least if src is already set
(the load of the image will start as soon as you set src).
So if the src is already set you need to determine two cases:
image already loaded
is still loading
This can be check with with HTMLImageElement.complete
Furthermore, you need to check for both the load or error case otherwise your loop might get stuck in case of a network error or when something else goes wrong with loading the image.
// create a function to which you can pass the image
// that function will determine if the image was already loaded
// or it has to wait for the image to be loaded
function waitForImage(image) {
return new Promise((resolve, reject) => {
// first check if image was already loaded
if (image.src && image.complete) {
// check if loading was successful
// there might be better ways then checking its dimension
// because this will fail if you load an image that has no size (like tracking pixels)
if (image.width > 0 || image.height > 0) {
resolve()
} else {
reject()
}
} else {
// resolve in case a successful load
image.onload = resolve
// reject in case of an error
image.onerror = reject
}
})
}
async function loadImages() {
// create some dummy data to make the snipped runable
let urls = [
'https://via.placeholder.com/350x150',
'https://via.placeholder.com/350x150',
'https://via.placeholder.com/350x150',
'https://via.placeholder.com/350x150',
'https://via.placeeeeeeeholder.com/350x150', // intentional typo to make it fail
'https://via.placeholder.com/350x150'
]
let images = urls.map(url => {
let image = new Image()
image.src = url
return image
})
// iterat over the images
for (let image of images) {
try {
// call a function that returns a Promise
// and use await wait for the image to load or fail loading
await waitForImage(image)
console.log(`loading ${image.src} finished`)
} catch (err) {
console.log(`loading ${image.src} failed`)
}
}
}
loadImages();

javascript handling multiple callback mutexes

I am fairly new to javascript. Let's say I have the following code.
let sources =["http://localhost:3001/resources/black_bish",""http://localhost:3001/resources/white_bish""]
let loaded_images=0
sources.forEach(ele = > {
let img = new Image()
img.src = ele
img.onload = function () {
loaded_images=++
}
})
Here I had a question about Javascript and concurrency. Can't the 2 callbacks be called at the same time similar to when working with threads? Here, won't there be a race condition? If I were to perform a different action than "loaded_images=++", is there any race condition like manipulating a data structure that I should be worried about?
Thank You
One way is to return a Promise for each image that you load. This promise will resolve, or in laymen terms: continue when the right condition is met, whenever the images has been loaded. It's like a return statement but instead of ending the function you continue to the next step.
Promise.all is a method on the promise constructor which takes in an array of promises. When all of the promises in the array have been fullfilled (meaning resolve has been called) then do something with the values of all of the promises.
const loadImage = (source) => new Promise(resolve => {
let img = new Image();
img.onload = function() {
resolve(img);
};
img.src = source;
});
const whenAllImagesAreLoaded = (...sources) => Promise.all(
sources.map(source => loadImage(source));
);
whenAllImagesAreLoaded('image-1.jpg', 'image-2.jpg').then((images) => {
images.forEach(image => {
console.log(image, ' is loaded');
});
});
Another example is also with promises in combination with the async / await syntax, which halt execution until the promise you are waiting for has been fulfilled.
This opens up the possibility to, for example: load images one after another, after the previous image has been loaded.
async function loadImagesSequentially(...sources) {
for (const source of sources) {
let image = await loadImage(source);
console.log(image, ' is loaded');
}
}
Both methods give you better control over how to handle race conditions, or eliminate them completely. I'd suggest that you practice as much as you can with promises. It is a very powerful tool in your JavaScript toolbox.
If you have any questions, please let me know.

Waiting for all elements to fully with Promise.all

I've been trying to figure out how Promises work with a rather simple example: one that fetches a number of images, loads it onto the page in order, counts the number of images loaded.
const addImg = url => {
fetch(url)
.then(validateResponse)
.then(readResponseAsBlob)
.then(showImage)
.catch(Error);
}
function showImage(responseAsBlob) {
const container = document.getElementById('img-container');
const imgElem = document.createElement('img');
container.appendChild(imgElem);
const imgUrl = URL.createObjectURL(responseAsBlob);
imgElem.src = imgUrl;
return imgUrl;
}
document.getElementById("add").onclick = () => {
document.getElementById("status").innerHTML = "Fetching...";
Promise.all(urls.map(url => addImg(url)))
.then(setTimeout(() => {
document.getElementById("status").innerHTML = document.getElementsByTagName("img").length + " images";
}, 0));
}
The addImg function fetches an image from the url, processes it as a blob and showImage renders adds a new img. When I try to add images from an array of urls, I have noticed a few problems I want to fix:
The images don't necessarily show up in order
the img count is not accurate
My first thought: if I deconstruct the addImg function so that it execute each step as a separate promise( fetch all -> then validate all -> then ... so on), it might work the way I intend it to, but I'm not sure if that's the right approach to it.
It might make more sense to you if you rewrote your code using async/await. If you rewrote your AJAX call as
const addImg = url => fetch(url)
.then(validateResponse)
.then(readResponseAsBlob)
.then(showImage)
.catch(Error);
And then you could do something like:
async function loadImages(){
for(image in imageList){
await addImg(image.url);
}
console.log('Images loaded');
}
This way your code will wait for each image load to complete before the next. Note that this isn't very performant but if you want them loading specifically in order then this is one you could achieve that easily.

dealing with async code on frontend

I'm pretty new to programming with JS and started with Node. I have been using the async module while working on the backend with the database to deal with async code. What should I be using to deal with async code on the front end. I have the following that checks an array of images to see if they load.
$(document).ready(function () {
var images = <%- JSON.stringify(images) %>;
var imagesNotLoading = [];
images.forEach(function (image) {
if(!imageLoads(image)) {
imagesNotLoading.push(image)
});
console.log(imagesNotLoading);
//returns true if image loads successfully
function imageLoads(src) {
var img = new Image();
img.onload = function() {
return true;
};
img.onerror = function() {
return false;
};
img.src = src;
}
});
The imageLoads function will return undefined or !undefined which is true. What should I be using to wait till the function has returned true or false before pushing it on to the imagesNotLoadingArray. Any help appreciated.
You should write the imagesNotLoading array on the error handler.
var images = <%- JSON.stringify(images) %>;
var imagesNotLoading = [];
images.forEach(function (image) {
imageLoads(image)
});
//returns true if image loads successfully
function imageLoads(src) {
var img = new Image(src);
img.onload = function() {
successFunction(img);
};
img.onerror = function() {
errorFunction(img);
};
img.src = src;
}
function errorFunction(img){
imagesNotLoading.push(img.src);
}
function successFunction(img){
console.log("Image "+img.src+" was loaded successful!");
}
If you're using the async-library with node, there is no reason why you shouldn't use the same in your browser to deal with asynchronous code. As soon as you load the browser version of the async library, you can use its async.parallel(...) function to check which of your images are loading and which aren't.
Apart from that, you might wanna take a look at promises, since they greatly simplify dealing with asynchronous code in Javascript - no matter whether in the front- or backend.
Using promises, your code would look something like
$(document).ready(function () {
var images = <%- JSON.stringify(images) %>;
var imagesNotLoading = [];
images.forEach(function (image) {
imageLoads(image).then(function(){}, function() {
imagesNotLoading.push(image);
});
});
// returns true if image loads successfully
function imageLoads(src) {
var img = new Image();
return new Promise(function(resolve, reject) {
img.onload = resolve;
img.onerror = reject;
img.src = src;
});
}
});
This will wait for all your images to either be loaded or errored and execute the callback afterwards. Since the onload and onerror functions are executed at some time in the future and you don't know yet when this is going to be, you're best of with promises to handle the result of image loading.

How to load images with chained Promises

I'm having trouble loading images with a script of chained promises - they turn up undefined by the end. I also have a 404 error I don't quite understand as I've confirmed that all the sources are valid, but admit I'm still a noob to debugging in an async context. Here is the fiddle in question. I've been chewing on this one for a couple days now and could use a nudge or two to get me headed in the right direction. I think this may be the problem area:
function loadImagePromises() {
var imageLoadPromises = [];
for ( var i = 0; i < sources.length; i++ ) {
imageLoadPromises.push(
imgLoad( sources[i] ).then(function(response) {
var myImage = new Image();
myImage.src = response; // response is a blob
}, function(Error) {
console.log("There was an error when the image was loaded after download: " + Error);
}) );
}
console.log( imageLoadPromises );
return imageLoadPromises;
}
As context, I'm writing an image loader script using promises for a Three.js program I have. No need to load the images to the DOM - I'll use them later as textures in a WebGL visualization.
NOTE:
Here's an earlier and simpler fiddle working end-to-end and outputting to the DOM.
Probably some other considerations here but your success handler isn't returning anything explicitly, so it's implicitly returning undefined:
function(response) {
var myImage = new Image();
myImage.src = response;
// if you want the promise to resolve to a useful value put
// it below, with whatever you want e.g:
return myImage;
}
RE: Errors:
You should probably not shadow Error in your error handler, just use lowercase error. Also, when using console.log/console.error you can use , to chain parts of the message which usually presents a richer message than the string used when concatenating.
e.g.
console.error("There was an error when the image was loaded after download: ", error);
FWIW you could also reduce some of the tedious iteration/collection here by using map to map each source to a promise:
function loadImagePromises() {
return sources.map(function(source) {
return imgLoad(source).then(function(response) {
// ...
});
}
EDIT Re: waiting for image objects to have actually loaded
return imgLoad(source).then(function(response) {
var imageURL = window.URL.createObjectURL(response);
var myImage = new Image();
return new Promise(function(resolve, reject) {
myImage.onload = function () {
// image has loaded
resolve(myImage);
}
// TODO error handling
myImage.src = imageURL;
});
}, console.error);
});

Categories

Resources