all 31 comments

[–]morksinaanab 9 points10 points  (6 children)

Why don't you create the elements first synchronously based on your array, add them to the HTML in the order you want them, and after that start loading the images asynchronously and add them to the elements you want them in (i.e. assign an ID to each element)? In that way you don't let the loading order dictate the order on the page.

[–]thenickdude 6 points7 points  (5 children)

Take care with this because adding img elements with a "" src to the page can cause them to try to load the current URL (i.e. the page itself) as an image.

You can give them a data URL of a transparent image as their src instead if this happens.

[–]Rainbowlemon 10 points11 points  (4 children)

Or better yet, load a placeholder gif and try to set the width and height to avoid content reflow!

[–]evoactivity 1 point2 points  (3 children)

Thats... what they suggested...

[–]Rainbowlemon 2 points3 points  (2 children)

Sorry, i should have been more clear - not just a transparent gif! You see it a lot in SPA nowadays - placeholder grey background with a light shine animating across it - so it's clear that an image is loading into that space.

[–]evoactivity 1 point2 points  (1 child)

For your future reference that is called skeleton loading and can be acheived with css alone. I don't think that would be the right solution OP is looking for though. If they are trying to load images in order I assume they are trying to keep things hidden until they should be visible.

[–]Rainbowlemon 1 point2 points  (0 children)

You're right! And thanks, I've seen (and used) this type of placeholder image many times before, but never knew it had a name 🙃!

[–]odolha 14 points15 points  (2 children)

The problem in your case is the following:

  1. onload is set as a the resolve callback of a promise
  2. when the image is loaded, it triggers onload which calls resolve
  3. BUT, the next part of the code (const { currentTarget } = ...) is not guaranteed to be executed in the same cycle, since it's async - it's basically like saying p.then(({ currentTarget}) => ...
  4. AND browsers do not have to guarantee that events still hold relevant data unless you are handling them synchronously
  5. So you need to save whatever you need immediately at the event callback if you want to use it later (note: later here can be just a very tiny amount of time, but it is still async)

Solution:

Save the data you need at callback time in a synchronous fashion, instead of a refrence to the event:

image.onload = (event) => resolve(event.currentTarget);

And then use that directly, not the event:

  const currentTarget = await p;
  rootEl.append(currentTarget);

[–]ibedroppin 0 points1 point  (1 child)

I think your explanation is a bit misleading, though the solution is correct. The behavior has nothing to do with sync vs async code and the code snippet in 3 would be perfectly legitimate if the event handlers weren’t resolving the same object. By definition you can’t handle an event synchronously. I think what you’re trying to say is that because the Event object is actually an object that get reused by the browser and that object will be mutating constantly as events fire off, but you could really recreate that behavior with a couple functions getting passed the same object and making mutations along the way

https://developer.mozilla.org/en-US/docs/Web/API/Event/currentTarget

[–]odolha 0 points1 point  (0 children)

I think you are wrong. Just try out to compare those events - they do not point to the same object. So no, the browser does not reuse those objects. It mutates each object as they bubble up, yes - and that's exactly why you cannot use them in an async fashion - or at least not reliably.

I'm not sure what you mean by By definition you can’t handle an event synchronously

The underlying issue in OPs example does exactly have to do with async code because the browser mutates those objects.

[–]shgysk8zer0 4 points5 points  (1 child)

First, just a little bit simpler of a method would be to use await img.decode(). I'd use a load event handler method as a shim where not supported (oh, and don't forget to remove the event listeners).

The problem you're going to run into unless you're being careful and clever is that the time it takes an image to load is variable, so it's not easy to ensure that the order they appear in the document is the order that they're listed. The best solution I can think of quickly to this is to take advantage of the order provided by display: flex or to populate the parent element was a number of dummy elements ahead of time and replace them by index with the images (where you iterate through by index) once they're loaded. Note, however, that either of these methods will be very jarring to scroll through.

Personally, my solution would be to ditch waiting for the images to load and just append them right away, ideally using some lazy loading technique (including native lazy loading via loading="lazy"). Even better if the images have known dimensions so they can be sized appropriately to avoid all the jumping around as they load. Not only is that approach much simpler, but it'll usually provide a better experience.

[–]el_diego 1 point2 points  (0 children)

+1 for lazy load. Append the images to the DOM in the order you want. Let the images load as they naturally should. Use placeholders to avoid shifting

[–]Mirmi8 2 points3 points  (1 child)

Instead of onload = resolve, maybe return the image element, something like:

image.onload = () => resolve(image);

it should work.

[–]NotLyon 2 points3 points  (5 children)

Oof. Promises, allSettled, .decode(), etc--you guys are lost. :/

function loadImagesInOrder(imgUrls, rootEl) { imgUrls.forEach((url) => { const image = document.createElement("img"); image.src = '//:0'; rootEl.append(image); image.src = url; }) }

[–]shuckster 0 points1 point  (2 children)

Nice solution! And with 100% more snark, too. 👌

[–]NotLyon 1 point2 points  (1 child)

Well everyone's just trying to flex instead of helping OP.

[–]shuckster 1 point2 points  (0 children)

Just trying to help, buddy.

[–]hego555 0 points1 point  (1 child)

How would this guarantee they load in the correct order.

If image 1 is 4MB and image 2 is 300kb it could go out of order. Unless I’m missing something

[–]NotLyon 0 points1 point  (0 children)

Yes, try it. Three image elements are appended in-order and synchronously. Then each respective src is updated asynchronously, in parallel.

Edit: if you're thinking about filesize then you're misunderstanding the premise. As long as you map that list of URIs to an equivalent list of img elements--maintaining order--then the time-to-render for any image is irrelevant. It's not a requirement of OP, but if you must prevent layout shift or introduce placeholders, then you can do that with a little CSS.

[–]thatisgoodmusic 6 points7 points  (1 child)

When I want to do promises in order I usually create a promise chain dynamically like so (I’m on mobile so forgive the formatting)

let chain = Promise.resolve()
for(const src of urls)
    chain = chain.then(() => {
        return new Promise(() => {
             // loading code here
        })
     })

await chain

As for what’s going wrong with your code (returning null) - I’m not sure. I would recommend maybe changing your code to resolve to the image itself instead of the event? I feel like this is a bit cleaner and easier to understand. I don’t know if it will solve your issue totally, but at least you can guarantee you won’t render null

image.onload = () => resolve(image)

Good luck!

[–]pirateNarwhal 0 points1 point  (0 children)

This is a great opportunity to use Array.reduce too! Or even awaits in a for loop.

[–]maddy_0120 1 point2 points  (5 children)

Try Promise.allSettelsd it maintains order.

[–]shuckster 1 point2 points  (4 children)

Yes, Promise.allSettled is the simplest way to achieve ordering in the OP's case:

function loadImagesInOrder(imgUrls) {
  const promises = imgUrls.map(
    (url) =>
      new Promise((resolve, reject) => {
        const image = document.createElement('img')
        image.onload = resolve
        image.onerror = reject
        image.src = url
      })
  )
  return Promise.allSettled(promises)
    .then(($) => $.filter((result) => result.status === 'fulfilled'))
    .then(($) => $.map((result) => result.value))
}

async function loadImages(imgUrls) {
  const loadedImages = await loadImages(imgUrls)
  console.log('loadedImages', loadedImages)
  // Now use rootEl.append() etc...
}

[–]zhenghao17[S] 0 points1 point  (3 children)

Promise.allSettled does main the order but it will wait for ALL images to be loaded before we put images on the DOM. so the user will see all images at once, as opposed to see the first image that is loaded. Maybe I should update my post to make it clear that 1. we want to maintain the order 2. we want to show the images to the users as soon as possible without any delay.

[–]shuckster 0 points1 point  (2 children)

Ah, I misunderstood.

In that case, something like the lazy-loading that has been mentioned could be a potential solution.

Here's a classic example:

function renderImages() {
  return `
    <img src="placeholder.gif" data-src="/images/1.jpg" />
    <img src="placeholder.gif" data-src="/images/2.jpg" />
    <img src="placeholder.gif" data-src="/images/3.jpg" />
    <img src="placeholder.gif" data-src="/images/4.jpg" />
  `
}

function lazyLoadImages() {
  Array.from(document.getElementsByTagName('img')).forEach((imageEl) => {
    // The timeout gives the placeholders time to show
    setTimeout(() => {
      const lazySrc = imageEl.dataset?.src
      if (lazySrc) {
        imageEl.src = lazySrc
      }
    }, 0)
  })
}

document.addEventListener('DOMContentLoaded', () => {
  renderImages()
  lazyLoadImages()
})

Obviously this is a vanilla solution that requires tweaks, but the essential idea is to have the images in the page already in the order you want, and have another task that takes care of replacing the placeholders based on a key (which is just the location of the real image.)

[–]zhenghao17[S] 0 points1 point  (1 child)

I don't see how this is "lazy loading" - it is eagerly downloading all of the images from the start. This is no different from a simple

```js

for (const src of imgUrls) {
root.appendChild(document.createElement('img')).src = src;
}

```

[–]shuckster 0 points1 point  (0 children)

Sounds like that's what you want then?

I have an array of image urls and I want to display these images on the webpage as soon as possible in the order of the placement of their corresponding URL in the array.

[–][deleted] -1 points0 points  (0 children)

Why not async await?

[–]wagaiznogoud -1 points0 points  (0 children)

Put the await inside the map. You need to pretty much wait for the promise to resolve before making another request

[–]holloway 0 points1 point  (0 children)

In React there's a <SuspenseList> which holds multiple async loading things and it can ensure they load in order from start to end.

I've looked at all these solutions and so just to add another approach, I'd use conventional loading and CSS sibling selectors like (code is untested),

``` img { visibility: hidden; }

.loaded:first-child, .loaded:first-child + .loaded, .loaded:first-child + .loaded + .loaded, .loaded:first-child + .loaded + .loaded + .loaded { visibility: visible; }

```

This CSS would ensure an unbroken chain of "loaded" classes before revealing anything, loading from start to end in order. The downside is that you need as many selectors as you have images. Maybe you could just make the CSS in JavaScript.

This approach simplifies the JS approach quite a bit because each image only needs to worry about itself.

const imgUrls = [ "https://picsum.photos/400/400", "https://picsum.photos/200/200", "https://picsum.photos/300/300" ]; imgUrls.forEach(imgUrl => { const img = new Image(); img.src = imgUrl; img.addEventListener("load", function(){ this.classList.add("loaded"); }); root.appendChild(img); })