DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’» is a community of 963,864 amazing developers

We're a place where coders share, stay up-to-date and grow their careers.

Create account Log in
Cover image for Svelte Lazy Image
Etienne
Etienne

Posted on • Updated on

Svelte Lazy Image

I recently decided to lazy load images on a Sapper 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-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. 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:

let observer
const elements = new Map()

Next we make sure this same IntersectionObserver is always used:

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:

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:

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:

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

<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
Furthermore we could optimize this approach by automatically compress images during bundling and using them as the placeholders.

Top comments (0)

This post blew up on DEV in 2020:

js visualized

πŸš€βš™οΈ JavaScript Visualized: the JavaScript Engine

As JavaScript devs, we usually don't have to deal with compilers ourselves. However, it's definitely good to know the basics of the JavaScript engine and see how it handles our human-friendly JS code, and turns it into something machines understand! πŸ₯³

Happy coding!