Svelte Lazy Image
Tags: javascript, svelte, webdev, lazy
Published: 2020-05-09 09:01:48

---
title: Svelte Lazy Image
published: true
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/0asciv1rqifqc3kq5wkr.jpg
description: Lazy load an image in Svelte with IntersectionObserver
tags: js,svelte,webdev,lazy
---
I recently decided to lazy load images on a [Sapper](https://sapper.svelte.dev/) powered website to optimize the initial loading time. I did this by using a placeholder URL and as soon as the image is visible to the user, replacing this placeholder URL with the real deal. Similar to the [gatsby-image](https://using-gatsby-image.gatsbyjs.org/)-approach (obviously not as sophisticated). Let me take you through the process :)

## 1. Detect when the image is visible
The first step is to make sure we can detect when an element (in our case an `img`) is first visible to the user. This can be achieved with the [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API). This API is really useful to detect intersections between an element and its ancestor or in our case the top-level viewport.

To prevent instantiating a new `IntersectionObserver` for each image we will write an intersection service. To do that, let's define a variable in which the `observer` will be saved and a `Map` which we will use to keep track of all the elements in the observer:
```js
let observer
const elements = new Map()
```

Next we make sure this same `IntersectionObserver` is always used:
```js
const getObserver = () => {
  if (!observer) {
    observer = initObserver()
  }
  return observer
}
```

The `initObserver` function is referenced in the previous snippet, but not yet implemented, let's do that:
```js
const initObserver = () => {
  return new IntersectionObserver((entries, observer) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        const lazy = entry.target
        observer.unobserve(lazy)
        if (elements.has(lazy)) {
          elements.get(lazy)()
          elements.delete(lazy)
        }
      }
    })
  })
}
```
The new observer watches intersections with each of it's targets which we will add soon. As soon as an intersection is detected (`entry.isIntersecting`) we don't need to observe the element anymore, since the real image URL is loaded. Then if we find the element in our map tracking the elements (which we normally should) we call the function saved in the map and delete the entry, since we don't use it anymore.

To observe an element we use the only function exported from this service: `observe`:
```js
export const observe = (element) => {
  const obs = getObserver()
  return new Promise((resolve) => {
    elements.set(element, resolve)
    obs.observe(element)
  })
}
```
The `observe` function returns a promise which is resolved as soon as the element is intersecting with the viewport (is visible).

## 2. Svelte Component
The next step is to implement a svelte component using this service to replace a placeholder URL with the real source. This is fairly simple:
```js
<script>
  import { onMount } from 'svelte'
  import { observe } from './intersection.service'

  let image, source

  onMount(async () => {
    source = placeholder
    await observe(image)
    source = src
  })

  export let src, placeholder, alt
</script>

<img src={source} {alt} bind:this={image} {...$$restProps} />
```
On mounting we set the image source to the placeholder and as soon as the image is visible we swap the sources. The `...$$restProps` is there to make sure, things like styles and other attributes get applied to the `img`.

## 3. Use it
The final usage is pretty simple:
```js
<Image
    alt="clouds"
    style="width: 100%"
    src="https://images.unsplash.com/photo-1587476821668-7e1391103e49?w=1600"
    placeholder="https://images.unsplash.com/photo-1587476821668-7e1391103e49?w=16" />
``` 

## 4. Next steps
To make sure this can be used in all browsers you are supporting you might need to add an [intersection observer polyfill](https://github.com/w3c/IntersectionObserver/tree/master/polyfill) 
Furthermore we could optimize this approach by automatically compress images during bundling and using them as the placeholders.