Lazy Loading Images in Svelte
Tags: svelte, webdev, javascript, html
Published: 2019-11-20 13:34:38

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


{% codesandbox lazy-loading-1-lt539 %}

We open the code and see that we have 3 friendly [Svelte](https://svelte.dev) 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:

```html
<script>
  import List from "./List.svelte";
</script>

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

<main>
  <h1>Kittens</h1>
  <List />
</main>
```

- `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:

```html
<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}

```

- `ListItem.svelte` is in charge of rendering an individual image inside an article tag:

```html
<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>

```

So we're loading and rendering a few images that are 300 pixels wide and 500 pixels tall from [placekitten.com](https://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:
```html
{#each items as item, i}
  <ListItem {item} lazy={i > 1} />
{/each}

```

In `ListItem.svelte`, we set the image `src`:
```javascript
export let item;
export let lazy;

let src = lazy ? '' : item;
```

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](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) 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](https://caniuse.com/#feat=intersectionobserver) (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.


```javascript
const observer = new IntersectionObserver(onIntersect);

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

```

We can observe (and unobserve) a node using a [Svelte action](https://svelte.dev/docs#use_action):

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

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

```

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

```html
<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>
```

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](http://g.recordit.co/4IL2u8eZiE.gif)

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`

```html
<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}

```

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

{% codesandbox lazy-loading-2-g6gcm %}

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

Thanks for reading! Don't hesitate to leave a comment or connect with me on [twitter](https://twitter.com/collardeau)!