DEV Community

Thomas Collardeau
Thomas Collardeau

Posted on

Lazy Loading Images in Svelte

Let's imagine we have ourselves a little web app that displays a column of images (of kittens, of course).

We open the code and see that we have 3 friendly Svelte components greeting us. Let's take a look at each one in turn:

  • App.svelte sets some basic styles and renders a List component. We won't be editing this file but here it is for clarity:
<script>
  import List from "./List.svelte";
</script>

<style>
  main {
    width: 300px;
    margin: 0 auto;
    text-align: center;
  }
</style>

<main>
  <h1>Kittens</h1>
  <List />
</main>
Enter fullscreen mode Exit fullscreen mode
  • List.svelte generates a list of images (such as https://placekitten.com/g/300/500?image=01) and renders a ListItem component for each of them:
<script>
  import ListItem from "./ListItem.svelte";

  // generate image data:
  const prefix = "https://placekitten.com/g/300/500?image=";
  const items = ["01", "02", "03", "04", "05"].map(num => prefix + num);
</script>

{#each items as item}
  <ListItem {item} />
{/each}

Enter fullscreen mode Exit fullscreen mode
  • ListItem.svelte is in charge of rendering an individual image inside an article tag:
<script>
  export let item;
  let src = item;
</script>

<style>
  article {
    width: 300px;
    height: 500px;
    margin-bottom: 0.5rem;
  }
</style>

<article>
  <img {src} alt='kitten'/>
</article>

Enter fullscreen mode Exit fullscreen mode

So we're loading and rendering a few images that are 300 pixels wide and 500 pixels tall from placekitten.com. Nice and easy.

The Issue At Hand

Most of the images (each being 500px tall) are naturally off screen when the user lands on the page. They might never scroll down to see all our awesome content below the fold. So they're downloading data for nothing on initial load, and slowing down their experience.

Even if they do scroll all the way down, it would be nice to load the images only when they are about to enter the viewport and lighten the initial load. We can improve the user's experience and serve fewer images on our end. Win-win.

When Lazy is Good

So let's lazy load our images! But not the first 2, we want to fetch those right away, and then load the rest as we scroll down.

First, let's have our List component pass down a lazy prop to ListItem, which will be true starting from the third image. When it's true, ListItem will set src to an empty string so that no image is requested at first.

In List.svelte, we pass down a new lazy prop:

{#each items as item, i}
  <ListItem {item} lazy={i > 1} />
{/each}

Enter fullscreen mode Exit fullscreen mode

In ListItem.svelte, we set the image src:

export let item;
export let lazy;

let src = lazy ? '' : item;
Enter fullscreen mode Exit fullscreen mode

So, at this stage, we're loading the first two images but the rest is never loading. How shall we trigger this effect?

Intersection Observer

The Intersection Observer is a web API that allows us to know when an element is intersecting (or about to intersect) with the viewport. It's got solid browser support (it's just not available in IE11).

How does it work? We create an observer using IntersectionObserver and give it a function that will run when a DOM node that we've registered is intersecting with the viewport.

const observer = new IntersectionObserver(onIntersect);

function onIntersect(entries){
  // todo: update relevant img src
}  

Enter fullscreen mode Exit fullscreen mode

We can observe (and unobserve) a node using a Svelte action:

<script>
  function lazyLoad(node) {
    observer.observe(node);
    return {
      destroy() {
         observer.unobserve(node)
      }
    }
  }
</script>

<article use:lazyLoad>
  <!-- img -->
</article>

Enter fullscreen mode Exit fullscreen mode

Putting it together our ListItem.svelte looks like this (minus the styles which haven't changed):

<script>
  export let item;
  export let lazy = false;

  let src = item;
  let observer = null;

  if (lazy) {
    src = "";
    observer = new IntersectionObserver(onIntersect, {rootMargin: '200px'});
  }

  function onIntersect(entries) {
    if (!src && entries[0].isIntersecting) {
      src = item;
    }
  }

  function lazyLoad(node) {
    observer && observer.observe(node);
    return {
      destroy() {
        observer && observer.unobserve(node)
      }
    }
  }
</script>

<article use:lazyLoad>
  <img {src} alt='kitten'/>
</article>
Enter fullscreen mode Exit fullscreen mode

When the lazy prop is passed in as true, we immediately set the src to empty string and create an observer. We add a rootMargin option so that the onIntersect function is triggered 200 pixels before the element comes into view. In lazyLoad, we register the article node that we want to watch.

Effectively, we are creating an observer with a single node for each ListItem, so we can check if that node (entries[0]) is in fact intersecting in our OnIntersect function and set src = item which will request the image.

And just like that, we're lazy loading our images! We can see in the devtools that we are not requesting all images upfront, as illustrated in this GIF:

lazy load demo

Last thing, Let's make sure our app doesn't blow up if intersectionObserver isn't available (IE11) by adding a hasAPI check in List.svelte

<script>
import ListItem from "./ListItem.svelte";

const prefix = "https://placekitten.com/g/300/500?image=";
const items = ["01", "02", "03", "04", "05"].map(img => prefix + img);
const hasAPI = "IntersectionObserver" in window; // new
</script>


{#each items as item, i}
  <ListItem {item} lazy={hasAPI && i > 1} />
{/each}

Enter fullscreen mode Exit fullscreen mode

Here is the updated sandbox shall you want to tinker with this code:

This is a technique I recently implemented for a painter's portfolio website that I built using Sapper. You can see it at https://john-hong-studio.com.

Thanks for reading! Don't hesitate to leave a comment or connect with me on twitter!

Top comments (2)

Collapse
 
igorfilippov3 profile image
Ihor Filippov • Edited

Hello. Thank You for this article. It was very helpful for me.
I want to add something. In current situation, we have an IntersectionObserver which never ends observing until node will be destroyed. But we need only one intersection. So I propose to edit your code a little bit.

function onIntersect(entries, observer) {
    if (!src && entries[0].isIntersecting) {
      observer.unobserve(entries[0].target);
      src = item;
    }
  }

We also can pass an observer through parameters props. Thanks to this, a lazyLoad action could be reused in many places.

Collapse
 
adrienwelter profile image
adrienwelter

Hello
Thanks for you code. However I have a question. Could you show me the code in order to access the images on my local server:
const prefix = "XXXXXX?image=";
Should I put the image folder in "/public" and must the folder structure be the same as in: "place kitten.com" ?
Thanks
Adrien