Skip to content

Image Lazy Loading in Svelte for Improved Performance

Published: at 22:22

Examples used in this post are available on my GitHub account.

Table of contents

Open Table of contents

What is lazy loading?

Lazy loading is a technique used to optimise web page loading times by deferring the loading of non-critical resources, such as images, until they are actually needed. This means that instead of loading all the images on a page at once, the images are only loaded when they are visible in the viewport or near it. This can greatly improve the perceived speed of a web page, as users will see the important content first, while the non-critical resources load in the background.

The benefits of lazy loading are particularly noticeable on web pages with a large number of images, such as photo galleries, e-commerce sites or blogs. These pages can be slow to load if all the images are loaded at once, and may even cause the page to crash on slower devices or networks. By using lazy loading, you can reduce the amount of data that needs to be downloaded and processed on page load, making the page faster and more responsive.

Lazy loading can be implemented using a variety of techniques, depending on the technology used to build the web page. For example, in vanilla JavaScript, you can use the IntersectionObserver API to detect when an element, such as an image, enters the viewport and then trigger its loading. Similarly, in popular front-end frameworks such as React or Vue.js, there are several libraries and plugins available that provide lazy loading capabilities. For example, react-lazyload for React and vue-lazyload for Vue.

Native lazy loading

In Svelte, a popular JavaScript framework for building web applications, there are several built-in features that make implementing lazy loading a breeze. One of the easiest ways to implement lazy loading in Svelte is to use the <img> element’s loading attribute. The loading attribute can take three values: lazy, eager, or auto. If you set the loading attribute to lazy, the browser will defer loading of the image until it is in the viewport or near it. If you set the loading attribute to eager, the image will be loaded immediately. If you set the loading attribute to auto, the browser will decide when to load the image based on its own criteria.

For example, consider the following Svelte component that displays an image:

// src/routes/native/+page.svelte
<script>
  import Image1 from '$lib/images/joshua-sortino-gii7lF4y0WY-unsplash.jpg';
</script>

<img src={Image1} alt="Column" width="1251" height="835" loading="lazy" decoding="async" />

In this example, we set the loading attribute of the <img> element to lazy, which will defer loading of the image until it is in the viewport or near it.

If the application from image-lazyload-sveltekit repository is running, navigate to http://localhost:5173/native. Open Chrome developer tools network tab and note that the browser downloaded 10.1 MB of resources. This is due to the images used being extremely large. This is solely for the purpose of the exercise and they should be optimised for production. With the dev tools on one side and the browser window on the other, start scrolling down the page until you are near the next image and then the next one. The browser will keep downloading images, as you scroll instead of all at the same time. Native lazy loading functionality is a very simple solution. However, it’s got it downsides.

Pros of native lazy loading:

Cons of native lazy loading:

Intersection observer pattern

Another approach to lazy loading images in Svelte is to use the IntersectionObserver API. The IntersectionObserver API allows you to observe when an element enters or leaves the viewport and take action accordingly. You can use this API to trigger the loading of an image when it enters the viewport, and to remove it from the DOM when it leaves the viewport.

To use the IntersectionObserver API in Svelte, you can create a new instance of the IntersectionObserver class and pass in a callback function to be executed when the observed element enters or leaves the viewport. Here’s an example:

// src/lib/components/IntersectionObserverImage.svelte
<script>
  import { onMount } from 'svelte';

  export let src;
  export let alt;
  export let width;
  export let height;
  export let placeholder = 'https://placehold.co/600x400';

  let observer;
  let ref;

  const options = {
    threshold: 0.5
  };

  onMount(() => {
    observer = new IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          entry.target.src = entry.target.dataset.src;
          observer.unobserve(entry.target);
        }
      });
    }, options);

    observer.observe(ref);
  });
</script>

<img src={placeholder} {alt} data-src={src} {width} {height} bind:this={ref} />

<style>
  img:not([src]):not([srcset]) {
    visibility: hidden;
  }
</style>

Open the following url http://localhost:5173/intersection-observer-pattern when the app is running. In terms of downloading resources, the behaviour will match the native implementation. When the user scrolls down the page, the browser will keep downloading images, however, with the slight difference. Intersection observer pattern supports the threshold option, which determines when the resource enters the view area. A single value shows the portion of the resource required in the view before the download starts, 0.5 being half of the image.

Something worth to bear in mind, this implementation of threshold demands the resource to have positive height value. If the resource has zero height it will be loaded instantaneously. On the other hand, this implementation of threshold isn’t particularly useful as the resource should ideally be loaded just before it enters the view, not after.

Pros of the Intersection Observer pattern:

Cons of the Intersection Observer pattern:

Vanilla-lazyload

Vanilla-lazyload is a lightweight, pure JavaScript library for lazy loading images, scripts, iframes, and other types of media. It’s designed to be easy to use and has no external dependencies, making it a great choice for developers who want a simple and flexible solution for lazy loading.

One of the key advantages of vanilla-lazyload is its flexibility. It supports a wide range of media types, and you can customise the library to fit your specific use case. For example, you can set custom data attributes for your images, which can be used to trigger the lazy loading process. You can also use custom events to trigger the loading of specific images or other media types. At the core, vanilla-lazyload uses intersection observer pattern but with additional features and lifecycle callbacks. So if you are leaning towards a more flexible solution, this may be a good option.

Another advantage of vanilla-lazyload is its small size. The library is only 2.5 KB when minified and gzipped, which makes it an excellent choice for projects that prioritise speed and performance. Despite its small size, vanilla-lazyload offers a number of advanced features, such as support for responsive images and the ability to use placeholder images while media is loading.

On the downside, Vanilla-LazyLoad doesn’t provide support for older browsers that don’t support the Intersection Observer API. This means that if you need to support older browsers, you’ll need to use a polyfill or fallback solution. Additionally, while the library is highly customisable, it may require some additional configuration to get up and running depending on your specific use case. Overall, however, Vanilla-LazyLoad is an excellent choice for developers who want a lightweight, flexible, and easy-to-use solution for lazy loading images and other media.

Here is how you can use it in Svelte:

Depending on your use case, start an instance of lazyload in a +page.svelte or +layout.svelte:

// src/routes/vanilla-lazyload/+page.svelte
<script>
  import Image1 from '$lib/images/joshua-sortino-gii7lF4y0WY-unsplash.jpg';
  import { browser } from '$app/environment';
  import lazyload from 'vanilla-lazyload';

  if (browser && !document.lazyloadInstance) {
    document.lazyloadInstance = new lazyload();
  }

  <img
      src="https://placehold.co/600x400"
      data-src={Image1}
      alt=“Describe your image"
      width="1251"
      height="835"
      class="lazy"
    />

</script>

As you can see the implementation is very similar to the previous example, with the exception of the class=“lazy”, it is a default css selector for vanilla-lazyload instance.

Another difference is the threshold implementation. It is a pixel value, which determines the distance between the resource and the view. This makes more sense for how the lazy load functionality is used in web apps.

Open http://localhost:5173/vanilla-lazyload in your browser with the app running. In Chrome dev tools network tab set network throttling to Slow or Fast 3G and scroll down. You will see how images when entering the view receive a loading animation in a form of moving borders. This is an example of using vanilla-lazyload callbacks.

if (browser && !document.lazyloadInstance) {
  document.lazyloadInstance = new lazyload({
    callback_loading: element => {
      element.parentElement.classList.add("lazyload-loading");
    },
    callback_loaded: element => {
      element.parentElement.classList.remove("lazyload-loading");
    },
  });
}

Bonus - Transformation and optimisation

Image transformation and optimisation are crucial steps in the web development process, as they ensure that images are appropriately compressed and optimised for web use, reducing page load times and improving overall performance. One tool that can help with this process is vite-imagetools, which is a plugin for the Vite build tool that provides a suite of image optimisation and transformation features.

With vite-imagetools, developers can easily transform and optimise images during the build process, making them more suitable for web use. For example, vite-imagetools can resize images, change their formats, and compress them using various techniques such as lossless or lossy compression. This can significantly reduce image file sizes, making them load faster and improving the overall user experience.

One of the significant advantages of using vite-imagetools is that it integrates seamlessly with the Vite build tool, allowing developers to optimise images as part of their build process, without the need for additional tooling or manual optimisation. This can save a significant amount of time and effort, particularly when working with large image sets.

Overall, vite-imagetools is an excellent tool for developers looking to optimise their images for web use, as it provides a range of powerful optimisation and transformation features, all within a straightforward and easy-to-use plugin for the Vite build tool. By using vite-imagetools, developers can ensure that their web pages load quickly and provide an optimal user experience for their users.

Let’s enhance the previous example:

// src/routes/vanilla-lazyload/+page.svelte
<script>
  import HeroImageResource from '$lib/images/joshua-sortino-f3uWi9G-lus-unsplash.jpg?w=2035&h=1358&webp';
  import { browser } from '$app/environment';
  import lazyload from 'vanilla-lazyload';

  export let data;

  if (browser && !document.lazyloadInstance) {
    document.lazyloadInstance = new lazyload();
  }
</script>

<img src={HeroImageResource} alt="Hero" width="1920" height="1281" />

This line here is particularly important:

import HeroImageResource from$lib/images/joshua-sortino-f3uWi9G-lus-unsplash.jpg?w=2035&h=1358&webp';

The vite-imagetools plugin resizes and changes the format of the original image from jpeg to webp, which reduces it’s size from near 10 MB to just 487 KB. Not bad.

External media resource handling

Sometimes the media resources aren’t stored locally, but are requested from a CDN or as part of API response. All of the image transformations in vite-imagetools are powered by sharp.

Sharp is a popular Node.js library used for image optimisation. It offers a lot of flexibility when it comes to optimising images for the web, and it can be used to optimise both external and local resources.

When optimising external resources, Sharp can be used to resize and compress images on-the-fly, reducing the amount of data that needs to be transferred over the network. This can improve website performance, especially on mobile devices with limited bandwidth.

Using Sharp for image optimisation can be a great way to improve website performance and user experience, but it is important to balance optimisation with image quality. Over-optimisation can lead to image degradation, which can negatively impact the user experience. It is important to test different optimisation settings and find the optimal balance between image quality and performance.

Because Sharp is a Node.js library it can only be run on the server in +page.server.js file:

// src/routes/vanilla-lazyload/+page.server.js
import sharp from "sharp";
import { error } from "@sveltejs/kit";

export async function load({ fetch }) {
  const res = await fetch(
    `https://res.cloudinary.com/dxsogfhc1/image/upload/v1681853834/cld-sample-3.jpg`
  );
  const buffer = await res.arrayBuffer();
  const data = await sharp(Buffer.from(buffer))
    .resize(998, 667, {
      fit: "cover",
      position: "center",
    })
    .webp()
    .toBuffer();

  if (res.ok) {
    return {
      props: {
        image: data.toString("base64"),
      },
    };
  }

  throw error(404, `Could not load ${res.url}`);
}

At the time of writing Sharp doesn’t support input as a url. To work around it is possible to download the resource as a buffer, do the conversion and return as base64 format, which can then be used as an image source:

<img
  src="https://placehold.co/600x400"
  data-src={`data:image/webp;base64, ${data.props.image}`}
  alt="Column"
  width="998"
  height="667"
  class="lazy"
/>

In conclusion, lazy loading and optimisation of images are crucial techniques for improving website performance and user experience. By deferring the loading of non-critical resources, such as images, until they are actually needed, lazy loading reduces the initial load time of a web page and improves its perceived speed. Meanwhile, optimisation techniques like compression and resizing can significantly reduce the size of image files, improving website performance and reducing the amount of bandwidth required to load them.

When implementing image optimisation techniques, it’s important to strike the right balance between image quality and performance. Over-optimisation can lead to image degradation and a poor user experience, while under-optimisation can lead to slow load times and poor website performance. Therefore, it’s crucial to test and fine-tune your image optimisation settings to ensure that your website is fast, responsive, and visually appealing.