SvelteKit Image Plugin: Next-Gen Images in Svelte
Tags: svelte, webdev, javascript
Published: 2021-09-08 07:51:33

---
title: "SvelteKit Image Plugin: Next-Gen Images in Svelte"
published: "true"
description: "SvelteKit Image Plugin: how to use the vite-imagetools plugin to bring responsive images in next-gen formats, like WebP, to your Svelte App."
tags: "svelte, webdev, javascript"
canonical_url: "https://rodneylab.com/sveltekit-image-plugin/"
cover_image: "https://dev-to-uploads.s3.amazonaws.com/uploads/articles/00ihptsxlgz1h20van1t.png"
---

## ✨ SvelteKit Image Plugin

After finding your feet with SvelteKit, probably one of the most sought after features is a SvelteKit image plugin.  If you are coming from a React background you might be thinking of something along the lines of <a aria-label="Open next/image docs" href="https://nextjs.org/docs/api-reference/next/image" >next/image</a> or <a aria-label="Open gatsby-plugin-image docs" href="https://www.gatsbyjs.com/plugins/gatsby-plugin-image/">gatsby-plugin-image</a>. The great news is that there is a plugin for generating Next-Gen, responsive images in SvelteKit. The bonus is you don't sacrifice SvelteKit's lightning fast development experience to use it.

In this post we take a look at how you can leverage the slick Vite environment at the heart of SvelteKit to generate Next-Gen, responsive images.  We also look at some techniques pioneered by image optimisation guru <a aria-label="Open Addy Osmani's Twitter page" href="https://twitter.com/addyosmani">Addy Osmani</a> to minimise Cumulative Layout Shift. If you you don't yet know what that is don't worry we'll visit some of the terms used here next.  We'll also have a brief introduction to the `async`/`await` API which you will find handy if you are new to JavaScript.

## 🧑🏽‍🎓 What do Responsive, Next-Gen, Cumulative Layout Shift and Lazy Loading all Mean?

Responsive is just a way of saying your app adjusts to the user device's display width.  This means the buttons are not so small that when you open the app on your phone it's difficult to press them.  Responsiveness is a ranking factor for Google mostly because responsive sites create great user experience.  Responsive images scale down to fit the browser window width (so you don't have to scroll across to the see the rest of the image or have it abruptly cropped).  For these devices, with smaller screens, we can safely serve a lower resolution image.  This will be smaller, allowing the page to load faster. Which brings us back to enhanced user experience.

### Next-Gen Images

Talking of serving smaller images, that's what Next-Gen images are all about.  WebP (which we focus on today) as well as AVIF and <a aria-label="Read about how AVIF format compares to J PEG X L" href="https://avif.io/blog/comparisons/avif-vs-jpegxl/">JPEG XL format images</a> are created using algorithms designed to preserve image quality while shrinking the image size.  Although WebP celebrates wider support that its newer cousins AVIF and JPEG XL, none of these are supported by older browsers.  This means to play it safe, when we serve Next-Gen images we also include a fallback in our markup.  The fallback is shown if the other formats are not supported by the user's browser.  You can set a fallback chain, trying JPEG XL first, then falling back to AVIF, then WebP and finally if all else failed JPEG (or PNG).

### Cumulative Layout Shift

Most popular sites today are well-designed.  There are still a few that can be a little frustrating to use because of Cumulative Layout Shift.  This is when the page is slow to load, you get impatient and hit a button while waiting to interact.  Just as you hit the button (but too late for you to pull out of the button press), the layout changes.  You push a button or click a link you didn't intend to. Now a page you have no interest in at all is loading (slowly) and you have to wait to go back, re-click and then wait again while the page you actually wanted loads.

What an awful user experience!  Which is why Cumulative Layout Shift is a core metric used by Google's page ranking algorithm.  Pictures can be a cause of CLS. We will see that if we add a placeholder (smaller file which takes up the same layout space as the ultimate image) we can eliminate Cumulative Layout Shift.

### Lazy Loading

This is important where you have a lot of content to load on a page.  Lazy loading is a way for us, as developers, to tell the browser which images to focus on and which it can wait until the user scrolls down.  Imagine an Instagram feed.  You can scroll down and eventually see hundreds of images. However when you first open up the feed, you only need to see a handful of images.  If the device tried to load all of the hundreds of images in the feed initially, you would have to wait forever even to see the handful of images at the top of the feed (as it tried to load all of the images).

A more sensible approach is to focus on loading the images in view and lazy load the others.  In other words, wait for the user to scroll down before loading another handful of images.  We can load our placeholders initially (these will be smaller that ultimate image) and defer loading the actual image until it is needed.

We will dovetail all these techniques together to create the ultimate user experience.  This should lead to higher ranking for our site pages.

## 🧱 What are We Building?

We'll start with the <a aria-label="Open the M D Svex Svelte Kit starter on Git Hub" href="https://github.com/rodneylab/sveltekit-blog-mdx">SvelteKit MDsveX starter</a> and add a responsive image to the home page.  We will see how you can generate a low resolution placeholder as well as an alternative dominant colour placeholder.  We will generate a WebP Next-Gen format image together with the original JPEG in different sizes so the image looks good on large retina displays without having to download a huge image for small mobile displays.  Using the placeholders, we will eliminate image layout shift.

If that all sounds good, then why don't we get cracking?

## 🚀 Getting Started

To get the ball rolling, <a aria-label="Open the M D Svex Svelte Kit starter on Git Hub" href="https://github.com/rodneylab/sveltekit-blog-mdx">clone the MDsveX SvelteKit starter</a> and get your local clone ready for development:

```shell
git clone https://github.com/rodneylab/sveltekit-blog-mdx.git sveltekit-image-plugin
cd sveltekit-image-plugin
pnpm install
cp .env.EXAMPLE .env
pnpm run dev
```

Here we clone the repo into a new directory, change into that directory, install packages and create some placeholder environment variables. Finally we spin up the dev server.  Swap `pnpm` for `npm` if you prefer npm or use the yarn equivalent, if you are a yarn person.

We will need a couple of packages later, let's install them now:

```shell
pnpm install -D sharp@0.28.2 vanilla-lazyload
```

We are installing the same version of sharp here which is used in `vite-imagetools` (see below).  I was getting some build errors when I tried to use the latest version (currently 0.29.1).  We will use sharp to help create the placeholder images.

We are also using <a aria-label="Open the N P M vite-iamgetools page" href="https://www.npmjs.com/package/vite-imagetools">`vite-imagetools`</a>. This is already included in the repo.  If you are following along on your own existing project, install `vite-imagetools` now.  Also see how to configure it in your `svelte.config.js` file (no need to do this if you cloned the starter as described above):

```javascript
/** @type {import('@sveltejs/kit').Config} */
import adapter from '@sveltejs/adapter-static';
import { imagetools } from 'vite-imagetools';
import { mdsvex } from 'mdsvex';
import preprocess from 'svelte-preprocess';
 
const config = {
  extensions: ['.svelte', '.md', '.svelte.md'],
  preprocess: [
    mdsvex({ extensions: ['.svelte.md', '.md', '.svx'] }),
    preprocess({
      scss: {
        prependData: "@import 'src/lib/styles/variables.scss';",
      },
    }),
  ],
  kit: {
    adapter: adapter(),
    files: {
      hooks: 'src/hooks',
    },
    // hydrate the <div id="svelte"> element in src/app.html
    target: '#svelte',
    vite: {
      define: {
        'process.env.VITE_BUILD_TIME': JSON.stringify(new Date().toISOString()),
      },
      plugins: [imagetools({ force: true })],
    },
  },
};
 
export default config;
```
## 🖼 Basic Image with vite-imagetools

Now let's add a JPEG, as a starting point.  First create a place for it in the project, by creating a the folder `src/lib/assets`. Then <a href="https://raw.githubusercontent.com/rodneylab/sveltekit-image-plugin/main/src/lib/assets/me.jpg">download the file `me.jpg`</a> and save it there.  That image is 1344&nbsp;pixels wide.  We will display the image at a maximum width of 672&nbsp;px and need the input to be double that to cater for retina displays.  Next step is to render the image, edit `src/routes/index.svelte`:


```svelte
<script>
  import meImage from '$lib/assets/me.jpg?width=672';
  import BlogRoll from '$lib/components/BlogRoll.svelte';
  import Card from '$lib/components/Card.svelte';
  import SEO from '$lib/components/SEO/index.svelte';
  import website from '$lib/config/website';
  // ...
</script>
```

Here we import the the image into the `meImage` variable.  Note we added a query string onto the end: `?w=672`. `vite-imagetools` will process the import and generate a 672&nbsp;pixel wide output for us.  This will be our base fallback image, we will include the 1344&nbsp;px wide retina image later.  SvelteKit will copy the image to the build directory and cache it.  For now, working on the dev server, the `meImage` variable just links to a temporary file.  You can see this if you `console log` it.  As the variable points to the image source we want to use, we can use it as the `src` attribute in an `img` tag.  Let's do that next:


```svelte
  <SEO {...seoProps} />
  <header>
    <h1>Climate &mdash; Sveltekit Starter</h1>
    <h2>SvelteKit MDsveX (Markdown for Svelte) Blog</h2>
  </header>
  <img
    alt={featuredImageAlt}
    loading="lazy"
    decoding="async"
    width="672"
    height="448"
    \src={meImage}
  />
  <Card>
  <h2><span>About me</span></h2>
  <p>
    I live and breathe analogue photography. I show you my favourite analogue film cameras on this
    site. Hopefully if you are not into analogue photography yet, some of my enthusiasm will rub off
    onto you.
  </p>
</Card>
```

There is a lot to remember in setting up responsive images and I find <a aria-label="Open the Addy Osmani tweet on responsive images" href="https://twitter.com/addyosmani/status/1388031656355794945/photo/1">this tweet from Addy Osmani</a> really helpful:

<a aria-label="Open the original tweet shown" href="https://twitter.com/addyosmani/status/1388031656355794945/photo/1">
![SvelteKit Image Plugin: Addy Osmani tweet showing how to embed a modern image in a web page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/366olnwomcsn8hfeb3vp.jpg)
</a>

### `img` Element Attributes

We will work towards something along those lines.  Anyway, going back to the `img` element we just created, you are probably already aware the `alt` attribute is important for users with visual impairments and describes the image.

Next, the `loading` attribute can be `eager` (default) or `lazy`. This determines whether the browser defers loading until the image is in the viewport. As a rule of thumb, the largest image, initially in the visible viewport (before the user scrolls down), should be `eager` loading.  This is so that the page looks interactive quicker.  Any other images can be set to `lazy`.  Here, since we want to see how to set up a lazy loading image in our example we will set it to lazy, even though our image is initially in the visible viewport.  `loading` is is not supported by all browsers so we have a couple more lazy loading tricks later.

The `decoding` attribute set to `async` hints that the browser should start decoding the image and immediately continue parsing the remaining content, rather than waiting for decode to complete before proceeding.  This is another measure to improve user experience, so we get more content visible quicker.

Finally, it is important to set `height` and `width`, to reduce cumulative layout shift.  By setting both height and width, the browser knows the aspect ratio of the image and can reserve the right amount of space for it in the layout while it waits for it to load. Read more <a aria-label="Open M D N docs on the i m g element" href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Img">about `img` element tags from MDN</a>.


Let's also update the styling of the image at the bottom of our home page file:

```svelte
<style lang="scss">
  header > h2 {
    font-size: $font-size-3;
  }
 
  img {
    border-radius: $spacing-1;
    margin: $spacing-6 $spacing-0 $spacing-12;
    max-width: 100%;
    height: auto;
  }
</style>
```

## 💫 Creating Low Resolution and Dominant Colour Placeholders

Placeholders are another trick for reducing cumulative layout shift.  To do this they need to be exactly the same size as the actual image (though they are typically lower resolution).  On top they make the page look more interesting, while the actual image loads, again improving user experience.

We will use the <a aria-label="Open the sharp site" href="https://sharp.pixelplumbing.com/">sharp imaging plugin</a> to generate the placeholders.  sharp will <a aria-label="Learn more about Base 64 encoding" href="https://en.wikipedia.org/wiki/Base64">encode these as Base64 strings</a> for us. We will make our placeholder generation functions available via a server endpoint.  Within the <a aria-label="Read about load functions in SvelteKit docs" href="https://kit.svelte.dev/docs#loading">load function</a> (first `script` block) on our page, we load the placeholders before we render the page.  Let's tackle things in that order.

Create an `image.js` file in the `src/lib/utilities` directory and add the following content:

```javascript
import sharp from 'sharp';
 
export const IMAGE_DIR = '/src/lib/assets/';
 
export const dominantColour = async ({ source }) => {
  try {
    const image = sharp(source);
    const { dominant } = await image.stats();
    const { r, g, b } = dominant;
    const buffer = await sharp({
      create: {
        width: 3,
        height: 2,
        channels: 3,
        background: { r, g, b },
      },
    })
      .jpeg()
      .toBuffer({ resolveWithObject: false });
    return \`data:image/jpeg;base64,\${buffer.toString('base64')}\`;
  } catch (error) {
    console.error('Error determining dominant colour: ', source);
  }
};
 
export const lowResolutionPlaceholder = async ({ source }) => {
  try {
    const image = sharp(source);
    const buffer = await image
      .resize(10)
      .jpeg({
        quality: 50,
        progressive: true,
        optimiseScans: true,
        chromaSubsampling: '4:2:0',
        trellisQuantisation: true,
        quantisationTable: 2,
      })
      .toBuffer({ resolveWithObject: false });
    return \`data:image/jpeg;base64,\${buffer.toString('base64')}\`;
  } catch (error) {
    console.error('Error generating low resolution placeholder: ', source);
  }
};
```

### `async`/`await` in Brief

We are using `async`/`await` here.  As an example, in line `8`, generating the image stats might not be immediate.  The result however is needed in the next line.  Normally JavaScript code runs asynchronously, which means it won't wait for the result of line `8` before starting work on line `9`, (it just starts execution and hopes for the best!).  This is different to how code generated by many other languages runs.

To help us out the sharp library authors implemented the `stats()` function to return a promise instead of the result.  A promise will either fail or eventually resolve to the result.  By including the keyword `await`, we tell the server not to continue to line `9` until the promise is resolved and we have a result.  Whenever we use the `await` keyword in a function, we also need to add the `async` keyword to the function signature (you can see this in line `5`).  Finally, since the promise can fail, it is a good idea to wrap the block in a `try`/`catch` block.

#### Where to Learn More

That is a very basic explanation. I found <a aria-label="See Jonas Schmedtmann Udemy course" href="https://www.udemy.com/course/the-complete-javascript-course/">Jonas Schmedtmann's Udemy course</a> fantastic on explaining some key JavaScript concepts (please don't pay the full price... you should be able to get it much cheaper if you set up a new account).  Also <a aria-label="Open M D N docs on Using Promises" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises">see MDN Using Promises documents</a> as a free alternative.

The `dominantColour` function creates us a 3&nbsp;px&nbsp;&times;&nbsp;2&nbsp;px image (same aspect ratio as our original).  It chooses the dominant colour from our input image and sets all six pixels to that colour.  Meanwhile, the `lowResolutionPlaceholder` function resizes the input to ten pixels wide and reduces the quality.  See <a aria-label="Open sharp docs on resizing images" href="https://sharp.pixelplumbing.com/api-resize">sharp API docs on resizing images</a> if you want to tweak the parameters for your own project. Both functions output a Base64 encoded string.

### Placeholder Endpoint

Next we want to create an endpoint which we can send our images to for processing and which uses our new functions.  Create a new folder in the project at `src/routes/api` and within the new directory add a `image-placeholders.json.js` file and add this content:

```javascript
import { IMAGE_DIR, dominantColour, lowResolutionPlaceholder } from '$lib/utilities/image';
import path from 'path';
const __dirname = path.resolve();
 
export const post = async (request) => {
  try {
    const { images } = request.body;
  
		const dominantColourPromises = images.map((element) => {
      const source = path.join(__dirname, IMAGE_DIR, element);
      return dominantColour({ source });
    });
  
    const placeholderPromises = images.map((element) => {
      const source = path.join(__dirname, IMAGE_DIR, element);
      return lowResolutionPlaceholder({ source });
    });
  
		const dominantColours = await Promise.all(dominantColourPromises);
    const placeholders = await Promise.all(placeholderPromises);
 
    return {
      body: JSON.stringify({ placeholders, dominantColours })
    };
  } catch (err) {
    console.log('Error: ', err);
    return {
      status: 500,
      error: 'Error retreiving data'
    };
  }
};
```

Our endpoint can process multiple images in a single request.  We feed in file names in an `images` array, it looks for these files in the `src/lib/assets` folder.  Once again we are using promises.  If you looked at promises for the first time in the code block above, you might want to gloss over the details here as this is next level promises.

In line `9` we are using <a aria-label="Read M D N docs on array dot map" href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map">`Array.map()`</a> to go through each element of the provided array of `images` and map the elements to a new output array; `dominantColourPromises`.  So the first element of `images` corresponds to the first element of `dominantColourPromises` and so on.  You might remember that `dominantColour` was an `async` function which means it returns a promise.  This means in the last line of the map function (line `11`), when we return the result of the call to `dominantColur`, we are actually adding a promise to the the output array, rather than the result.  `dominantColourPromises` is in fact an array of promises (the name should make sense now).

### `Promise.all()`

JavaScript has an efficient way to handle an array of promises, so we do not have to wait for the promise in the first element `dominantColourPromises` to resolve before we start processing the second.  We can start them running immediately one after the other.  This is convenient here as there is no link between the dominant colour of one image and the next.  We just need to make sure all of the promises are resolved before we return the entire array.  That is what the code in line `19` does.  It takes the input array of promises and once they have all resolved, generates an output array of the results.  As a warning, this works fine with `Array.map()`, though if we needed to use `Array.forEach()` instead (and some other array methods), we would have to handle promises differently.

For our example we will pass in an array containing a single image, though the code can handle multiple images efficiently.  The endpoint responds with an object containing two arrays: `placeholders` and `dominantColours`.  These both map to the input `image` array and are returned in line `23`.  Speaking of which let's next call the endpoint from our home page Svelte file.

## 📱 SvelteKit Image Plugin: Making it Responsive

We can easily generate images in multiple formats and sizes using the `vite-imagetools` plugin.  However for generating our placeholders we will use our endpoint.  In theory you can create a low-resolution placeholder with `vite-imagetools` by just setting image width to 10 pixels in the query string.  However, we have created functions, so that you can see how to do it yourself.  On top that trick wouldn't generate a dominant colour placeholder.

Next in our load function we will call the new endpoint to pull in the placeholders.  Following that, we will generate the responsive images using `vite-imagetools`.  Finally we set the placeholders to display initially and until the actual image loads.

First update the load function in `src/routes/index.svelte`:

```svelte
<script context="module">
  /**
   * @type {import('@sveltejs/kit').Load}
   */
  export async function load({ fetch }) {
    const url = './index.json';
    const postsPromise = fetch(url);
    const placeholdersPromise = fetch('/api/image-placeholders.json', {
      method: 'POST',
      credentials: 'same-origin',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        images: ['me.jpg'],
      }),
    });
 
    const [postsResponse, placeholdersResponse] = await Promise.all([
      postsPromise,
      placeholdersPromise,
    ]);
 
    if (postsResponse.ok && placeholdersResponse.ok) {
      const { posts } = await postsResponse.json();
      return {
        props: { posts, ...(await placeholdersResponse.json()) },
      };
    }
 
    return {};
  }
</script>
 
<script>
...
</script>
```

In line `15` we can give all the images we want to generate placeholders for.  For our case, it is just the one.  In line `27` we make the placeholder available in the following `script` block.

Let's generate the responsive images now, using `vite-imagetools` in the main script block:

```svelte
<script>
  import meImageSrcsetWebp from '$lib/assets/me.jpg?width=1344;672;336&format=webp&srcset';
  import meImageSrcset from '$lib/assets/me.jpg?width=1344;672;336&srcset';
  import meImage from '$lib/assets/me.jpg?width=672';
  // ...
</script>
```

As before, we use query parameters to tell <code>vite-imagetools</code> what to generate.  Here we generate a <code>srcset</code> which is needed to create a responsive image.  The <code>srcset</code> is just there to tell the browser what image sizes are available.  In our case we have three image widths which are double width, for retina displays, 672&nbsp;pixel wide (for desktop) and half-width, for mobile.  You can add more, depending on common display widths used by your site visitors.  In the two highlighted lines, <code>vite-imagetools</code> generates <code>srcset</code> attributes for WebP and JPEG which we can use in a <code>picture</code> element on our page.  Check the <a aria-label="Open vite-image tools docs" href="https://www.npmjs.com/package/vite-imagetools#usage"><code>vite-imagetools</code> documentation for a full list of available query parameters</a>.

### Responsive Markup

Let's wrap our <code>img</code> element in a <code>picture</code> element and add the responsive images.  We also define <code>sizes</code> in the last line of the script block.  This serves as a guide to browsers to choose the right responsive image source based on display width.  The <a aria-label="Open M D N documentation on responsive images" href="https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images">syntax is similar to the CSS media query</a> syntax.  Essentially here we tell the browser that the image width will be equal to the viewport width less our margin.  This is in the case that the viewport is smaller than 672&nbsp;pixels wide.  Otherwise the image is 672&nbsp;pixels wide.  The information is handy because it lets the browser download the right images while it is still working out the layout.

```svelte
<script>
  // ...
  const seoProps = {
    title,
    slug: '',
    entityMeta,
    datePublished: '2021-07-07T14:19:33.000+0100',
    lastUpdated: '2021-07-07T14:19:33.000+0100',
    breadcrumbs,
    metadescription,
    featuredImage,
    ogImage,
    ogSquareImage,
    twitterImage,
  };
  const sizes = '(max-width: 672px) calc(100vw - 32px), 672px';
</script>
 
<SEO {...seoProps} />
<header>
  <h1>Climate Sveltekit Starter</h1>
  <h2>SvelteKit MDsveX (Markdown for Svelte) Blog</h2>
</header>
<picture>
  <source {sizes} srcset={meImageSrcsetWebp} type="image/webp" width="672" height="448" />
  <source {sizes} srcset={meImageSrcset} type="image/jpeg" width="672" height="448" />
  <img
    class="lazy"
    alt={featuredImageAlt}
    loading="eager"
    decoding="async"
    width="672"
    height="448"
    \src={meImage}
  />
</picture>
<Card>
```

Here we add the `srcset` attributes, specifying the WebP image first, then the JPEG.  The order is important here.  The browser tries the sources in the order they appear.  If it does not support WebP, it tries the next source which is JPEG and settles for that as a fallback.  If you included AVIF and JPEG XL too, since JPEG XL is new and enjoys less support than AVIF and WebP the order would be JPEG XL, AVIF, then WebP and finally JPEG as a fallback.

Take a look at the page in your browser and right-click and save the image.  If you have WebP support in your browser you should see it saves a WebP format image.  The final piece missing is the placeholders.  Let's get those in and wrap up.

## 🥱 Adding Lazy Loading

We will use `vanilla-lazyload` to do the heavy lifting with lazy loading.  It makes use of the <Link aria-label="See how you can use the Intersection observer A P I in Svelte Kit" to="/tracking-page-views-sveltekit/">Intersection Observer API</Link> so the browser shows placeholders until the image is within the visible viewport. You will still see the blur-up effect as the image loads.

To use `vanilla-lazyload`, we need some JavaScript code to load on every page with lazy loading content.  On most sites, this would be every page so we can place the code in the layout component.  Edit `src/routes/__layout.svelte` to import the plugin and then create an instance if one does not yet exist:

```svelte
  ...
  import '$lib/styles/normalise.css';
  import '$lib/styles/index.scss';
  import { browser } from '$app/env';
  import lazyload from 'vanilla-lazyload';
```

```svelte
<script>
  //...
  export let post;
 
  if (browser && !document.lazyloadInstance) {
    document.lazyloadInstance = new lazyload();
  }
 
  $: isBlogPost = post !== undefined;
</script>
```

`browser` is a boolean which is true when code is running in the browser as opposed to on the SvelteKit server.  This is needed here because accessing the `document` object on the server would throw an error.

Let's return to the home page code and finish things off.  We need to import the `dominantColours` and `placeholders` props which we created in the load function.  We also need to tell our lazyload code to update when the pages loads:

```svelte
<script>
  import { browser } from '$app/env';
  import meImageSrcsetWebp from '$lib/assets/me.jpg?w=1344;672;336&format=webp&srcset';
  import meImageSrcset from '$lib/assets/me.jpg?w=1344;672;336&srcset';
  import meImage from '$lib/assets/me.jpg?w=672';
  import BlogRoll from '$lib/components/BlogRoll.svelte';
  import Card from '$lib/components/Card.svelte';
  import SEO from '$lib/components/SEO/index.svelte';
  import website from '$lib/config/website';
  import { onMount } from 'svelte';
 
  export let dominantColours;
  export let placeholders;
  export let posts;
 
  onMount(() => {
    if (browser) {
      document.lazyloadInstance.update();
    }
  });
  // ...
</script>
```

Now when the pages loads, `vanilla-lazyload` will look for `img` elements with the `lazy` class (it also <a aria-label="See short video on using vanilla-lazyload on i frames" href="https://rodneylab.com/lazy-loading-iframes-sveltekit/">works on iframes, as you can see in this short video</a>).  Initially we want to show the placeholder image so, we set out `img` element's `src` to the placeholder image.  Although we are working with Base64 images encoded in a string, instead of a path to an image file we can use the `src` attribute in exactly the same way.  `vanilla-lazyload` determines when it is time to load the final image, replacing the placeholder.  For it to know what the final attribute should be, we tweak the code slightly, so where we would have `sizes`, `srcset`, and `src` normally, we use `data-sizes`, `data-srcset` and `data-src`.  `vanilla-lazyload` swaps these out for the right content when it is time to load the image.  Let's update the picture element:

```svelte
<SEO {...seoProps} />
<header>
  <h1>Climate &mdash; Sveltekit Starter</h1>
  <h2>SvelteKit MDsveX (Markdown for Svelte) Blog</h2>
</header>
<picture>
  <source
    data-sizes={sizes}
    data-srcset={meImageSrcsetWebp}
    type="image/webp"
    width="672"
    height="448"
  />
  <source
    data-sizes={sizes}
    data-srcset={meImageSrcset}
    type="image/jpeg"
    width="672"
    height="448"
  />
  <img
    class="lazy"
    alt={featuredImageAlt}
    loading="eager"
    decoding="async"
    width="672"
    height="448"
    data-src={meImage}
    \src={dominantColours[0]}
  />
</picture>
```

## 💯 SvelteKit Image Plugin: Testing it out

If you hit refresh on the page, you should see the dominant colour flash and then the actual image.  Inspect the `picture` element in your browser.  You will see the plugin has worked its magic and the `src` and `data-src` attributes match.  Similarly, it will have added  `sizes` and `srcset` attributes.

Next lets replace `dominantColours[0]` with `placeholders[0]` in line `139`.  Remember we returned an array of placeholders, since we want the first element in that array (of just one image in our case), we use index `0`.

If your image looks a little grainy on the dev server, I noticed this too.  However when I pushed to a live server and published the demo site for this post it looked fine.

## 🔌 SvelteKit Image Plugin: Image Component

It could get tiresome including all the boilerplate code on every image of our site.  As an alternative, you might consider wrapping up some of the functionality into a component.  There is an <a aria-label="Open svelte kit components on N P M" href="https://www.npmjs.com/package/@rodneylab/sveltekit-components">`Image` component which does exactly this in `@rodneylab/sveltekit-components`</a>:

```
<script>
  import { Image } from '@rodneylab/sveltekit-components';
</script>
 
<Image {alt} {width} {height} {src} {sources} {placeholder} {sizes} style={'border-radius:12px'} />
```

You can <ExternalLink aria-label="Open svelte kit components Git Hub page" href="https://github.com/rodneylab/sveltekit-components">see docs on the component's Git Hub page</ExternalLink>.

## 🙌🏽 SvelteKit Image Plugin: Wrapup

In this post we:

- had an introduction to `async`/`await` in JavaScript,

- saw how to generate dominant colour as well as low resolution placeholders using the sharp image processing plugin,

- learned how to add cached, Next-Gen, responsive images with no cumulative layout shift using a SvelteKit image plugin.

You can see the <a aria-label="See the tutorial code on the Rodney Lab Git Hub page" href="https://github.com/rodneylab/sveltekit-image-plugin/">full code for this tutorial on the Rodney Lab GitHub page</a>.  There is <a aria-label="Open the demo site" href="https://sveltekit-image-plugin.rodneylab.com">also a demo site</a>.

I really do hope you found this SvelteKit Image plugin article useful.  You will note we included a static import of the image file in the page Svelte code.  This will be perfect for a lot of situations.  Where you render a page using a template, and the image included is different for each invocation of the template (for example the banner image in a blog post), you will need to use a dynamic import.  That way you can pull in the right image for each page at build time, without having to include all possible images in the template.  We will look at how to do this in this <a aria-label="Open post on SvelteKit dynamic image import" href="https://rodneylab.com/sveltekit-dynamic-image-import/">post dedicated to SvelteKit dynamic image import</a>.

## 🙏🏽 SvelteKit Image Plugin: Feedback

Have you found the post useful? Which other hosting service would you like to know how to host a SvelteKit site on? Would you like to see posts on another topic instead? Get in touch with ideas for new posts. Also if you like my writing style, get in touch if I can write some posts for your company site on a consultancy basis. Read on to find ways to get in touch, further below. If you want to support posts similar to this one and can spare a few dollars, euros or pounds, please <a aria-label="Support Rodney Lab via Buy me a Coffee" href="https://rodneylab.com/giving/">consider supporting me through Buy me a Coffee</a>.

Finally, feel free to share the post on your social media accounts for all your followers who will find it useful. As well as leaving a comment below, you can get in touch via <a aria-label="Contact me on Twitter" href="https://twitter.com/messages/compose?recipient_id=1323579817258831875">@askRodney</a> on Twitter and also <a aria-label="Contact Rodney Lab via Telegram" href="https://t.me/askRodney">askRodney on Telegram</a>. Also, see <Link aria-label="Get in touch with Rodney Lab" to="/contact">further ways to get in touch with Rodney Lab</Link>. I post regularly on <a aria-label="See posts on svelte kit" href="https://rodneylab.com/tags/sveltekit/">SvelteKit</a> as well as other topics. Also <a  aria-label="Subscribe to the Rodney Lab newsletter" href="https://rodneylab.com/about/#newsletter">subscribe to the newsletter to keep up-to-date</a> with our latest projects.