How to dynamically add images to webpage as soon as they're loaded?

Viewed 198

I have a webpage which displays a large number of dynamic images (100+) in a masonry format via Bootstrap 4. Currently, my function waits until all images are loaded before rending the page.

I am fetching the list of image assets with getImages() and then adding them to the webpage dynamically with addElements(). Here is my code:

const init = async () => {
  // get image URLs from backend service - returns a list of images files
  const getImages = async () => {
    let res = await fetch('/refresh', {method: 'POST'})
    res = await res.json()
    return res.data
  }

  // dynamic add images to webpage
  const addElements = async () => {
    const container = document.getElementById('content')
    const data = await getImages()
    for (let [index, image] of data.entries()) {
      let card = document.createElement('div')
      let img = document.createElement('img')
      img.setAttribute('src', image.path)
      card.appendChild(img)
      container.appendChild(card)
    }
  }
  await addElements()
}
window.onload = init

Since the order of the images on the webpage does not matter, I would like the images to appear on the page as soon as they are loaded, whereas currently no images appear on the page until addElements() returns. I'm assuming my current unwanted behavior has to do with the await addElements(), but I am confused on how to resolve the issue. Is it possible to render/add each image to the page as soon as it is loaded? How can I modify my example to accomplish this?

1 Answers

While your calls to the server are async, in the end the loop to add the img tags is run synchronously. The event loop will only get another change to take on the next task after your loop is finished.

You could set the loading property to "lazy"
That will still add all images to your DOM in the same order but the page will not wait for all images to be loaded.

What you can alternatively do is create the img tag but not add the element to the DOM yet. Instead wire-up the load event so you can add the img tag to the DOM when the load event fires and that happens once the image is actually fetched.

Here is the relevant detail:

  let img = document.createElement('img')
  // wire-up the load event
  img.addEventListener('load', () => {
    let card = document.createElement('div')
    card.appendChild(img)
    container.appendChild(card)   
  })
  // this will kick off loading of the image
  img.setAttribute('src', user.profile_image)  

And here is a complete working snippet based of your code to see the effect. Notice this loads the top 100 Users from Stack Overflow and uses their profile images as image file.

Jon Skeet should be first, right?

const init = async () => {
  // get image URLs from backend SEDE - returns a list of imgur ids
  const getImages = async () => {
    let res = await fetch('https://api.stackexchange.com/2.3/users?pagesize=100&order=desc&sort=reputation&site=stackoverflow&filter=!-.x5(BFepcUj', {method: 'GET'})
    res = await res.json()
    return res.items;
  }

  // dynamic add images to webpage
  const addElements = async () => {
    const container = document.getElementById('content')
    const data = await getImages()
    for (let [index, user] of data.entries()) {
      let img = document.createElement('img')
      img.addEventListener('load', () => {
        let card = document.createElement('div')
        card.appendChild(img)
        container.appendChild(card)   
      })
      img.setAttribute('src', user.profile_image)     
    }
  }
  await addElements()
}
window.onload = init
img {
 object-fit: scale-down;
 width:10vw;
 height:10vh;
 padding:10px;
 margin:2px;
 box-shadow: 2px 2px 5px grey;
}

#content > div {
  display: inline-block;
}
#content {
 width:100%;
}
<div id='content'>

</div>

Related