hop.ie

Lazy loading images in Svelte

June 29, 2020

When we build websites and apps we want them to load quickly and feel quick when navigating. One easy way to improve the speed is to only download visible images.

In this article I’ll show how can make use of the Intersection Observer alongside the onLoad event to load only the necessary images as our visitors load and then scroll within our Svelte websites and apps.

This article was originally published on CSS Tricks.

Real-life example

I put this approach together while testing the speed on Shop Ireland. It is a Svelte and Sapper application designed to be as fast-loading as I can make it.

During performance testing, the biggest issue was the home page loading many images at once, most of which were not even visible until the visitor scrolled.

Download the finished code

If you’d like to save some time you can download the final code for this demo from Github and read along for an explanation of how it works.

What we will build

Here’s the result we will be aiming for today.

See the Pen Lazy Loading Images in Svelte by Donovan Hutchinson (@donovanh) on CodePen.

Starting app

You might have an app in place where you’d like to apply these ideas, but if not you can start a new Svelte project and work on it locally. Start by initiating a new Svelte project and running it locally:

npx degit sveltejs/template my-svelte-project
cd my-svelte-project
npm install
npm run dev

You should now have a beginner app running on http://localhost:5000.

Adding the components folder

The initial Svelte demo has an App.svelte file but no components yet. Let’s set up the components we need for this demo. First, create a components folder in your src folder.

Within this create an Image folder, which will hold our components.

We’re going to have our components do two things. First we’ll want to check when our image is visible on the screen, and then if it is, we want to wait until the image file has loaded before then showing it.

To do this we’ll wrap an Intersection Observer around an Image Loader. This will allow both components to do one thing and create the effect we want. We’ll begin by setting up a wrapper to tell us when the component enters the viewport.

Intersection Observing

To avoid getting to deep into how Intersection Observer works, we’ll make use of a handy Svelte component that Rich Harris put together for the Svelte website.

We’ll set up this as a useful component first then look at what it does. Save the following into components/Image/IntersectionObserver.svelte:

<script>
	import { onMount } from 'svelte';

	export let once = false;
	export let top = 0;
	export let bottom = 0;
	export let left = 0;
	export let right = 0;

	let intersecting = false;
	let container;

	onMount(() => {
		if (typeof IntersectionObserver !== 'undefined') {
			const rootMargin = `${bottom}px ${left}px ${top}px ${right}px`;

			const observer = new IntersectionObserver(entries => {
				intersecting = entries[0].isIntersecting;
				if (intersecting && once) {
					observer.unobserve(container);
				}
			}, {
				rootMargin
			});

			observer.observe(container);
			return () => observer.unobserve(container);
		}

		function handler() {
			const bcr = container.getBoundingClientRect();

			intersecting = (
				(bcr.bottom + bottom) > 0 &&
				(bcr.right + right) > 0 &&
				(bcr.top - top) < window.innerHeight &&
				(bcr.left - left) < window.innerWidth
			);

			if (intersecting && once) {
				window.removeEventListener('scroll', handler);
			}
		}

		window.addEventListener('scroll', handler);
		return () => window.removeEventListener('scroll', handler);
	});
</script>

<style>
	div {
		width: 100%;
		height: 100%;
	}
</style>

<div bind:this={container}>
	<slot {intersecting}></slot>
</div>

This component will give us an ability to wrap other components, and it will determine for us whether the wrapped component is visible (intersecting) within the viewport.

If you’re familiar with the structure of Svelte components, you’ll see it follows the script / style / markdown pattern. To begin with, it sets some options that we can pass in. These are a once property, along with numeric values for the top, right, bottom and left distances from the edge of the screen.

We’ll ignore the distances but instead make use of the once property. This will ensure the images only load once, as they enter the viewport.

The main logic of the component is within the onMount section. This sets up our observer, which is used to check our element to determine if it’s “intersecting” with the visible area of the screen.

It also attached a scroll event to check whether the element is visible as we scroll, and then it’ll also remove this listener if we’ve determined that it is viable and once is true.

Loading the images

Let’s use our IntersectionObserver.svelte component to conditionally load our images by wrapping it around an Image Loader component. Within components/Image create a new file called ImageLoader containing:

<script>
  export let src
  export let alt

  import IntersectionObserver from './IntersectionObserver.svelte'
  import Image from './Image.svelte'

</script>

<IntersectionObserver once={true} let:intersecting={intersecting}>
  {#if intersecting}
    <Image {alt} {src} />
  {/if}
</IntersectionObserver>

This component takes some image-related props (src, alt) which we will use to create our image tag. It them imports two other components, the IntersectionObserver which we created already, and a new component we’ll create in a moment called Image.

We then make use of the IntersectionObserver component. This has a couple of interesting things going on. First we are setting once to true, so the image only loads the first time we see it.

Then we make use of Svelte’s slot props.

Slot Props

When we make use of a wrapping component we sometimes want to pass properties to the childen of the wrapper. Svelte gives us a way to do this called slot props.

In our IntersectionObserver component you may have noticed this line:

<slot {intersecting}></slot>

This is passing the intersecting prop into whatever component we give it. In this case, our ImageLoader component will receive this when it uses the wrapper.

We access this prop using let:intersecting={intersecting} like so:

<IntersectionObserver once={true} let:intersecting={intersecting}>

We can then use the intersecting value to determine whether to show the Image component. In this case we’re using an if condition to decide whether the show the image:

<IntersectionObserver once={true} let:intersecting={intersecting}>
  {#if intersecting}
    <Image {alt} {src} />
  {/if}
</IntersectionObserver>

If we do, we show the Image component and pass in the alt and src props.

You can learn a bit more about slot props in this tutorial.

We now have the code in place to show an Image component when it is scrolled onto the screen. Let’s build the component itself.

Showing image on load

In the components/Image folder make a new file called Image.svelte. This component will receive our alt and src props, it’ll then set up an img tag, listen for when it loads and then fade it in.

Set up the component like so:

<script>
  export let src
  export let alt

  import { onMount } from 'svelte'

  let loaded = false
  let thisImage

  onMount(() => {
    thisImage.onload = () => {
      loaded = true
    }
  })

</script>

<style>
  img {
    height: 200px;
    opacity: 0;
    transition: opacity 1200ms ease-out;
  }
  img.loaded {
    opacity: 1;
  }
</style>

<img {src} {alt} class:loaded bind:this={thisImage} />

Here we are getting the alt and src props. We also set up a couple of other variables, loaded to store whether it’s loaded yet, and thisImage to store a reference to the img DOM element itself.

We also make use of a helpful Svelte method, onMount. This gives us a way to call functions when our components have been mounted in the browser. In this case we’ll set up a callback for thisImage.onload. This will be executed when the image has finished loading, and it will set loaded to true.

To actually show the loaded image we’ll use CSS. The initial state of the image will be to have opacity of 0. Then when loaded, we’ll set opacity to 1. A transition takes care of animating this from transparent to visible. We’re using a very slow transition time of 1200ms in this demo, but you might want to set it to something like 200ms to make it more subtle.

With all that set up, we apply our image tag like so:

<img {src} {alt} class:loaded bind:this={thisImage} />

This uses class:loaded to conditionally apply a loaded class if the loaded variable is true.

It lastly uses the bind:this method to associate this DOM element with the thisImage variable.

Using our ImageLoader

Let’s actually use our component. Back in the App.svelte file we can import out component and use it like so:

<script>
  import ImageLoader from './components/Image/ImageLoader.svelte';
</script>

<ImageLoader src="OUR_IMAGE_URL" alt="Our image"></ImageLoader>

Working demo

You can download the complete code for this demo on Github. Here’s a live example:

See the Pen Lazy Loading Images in Svelte by Donovan Hutchinson (@donovanh) on CodePen.

See it in the wild

This approach is being used on my Amazon Ireland project on the home page, category pages and search pages to help make every page view faster. I hope you find it useful!

Well that’s enough about me. Your turn!

Have you build a cool Svelte app you’d like to tell me about? You can message me on Mastodon, I’d love to hear from you.