CleverImage Astro Component for My Responsive Images
Tags: webdev, astro, performance, typescript
Published: 2023-05-12 16:02:14

Create responsive images in pure HTML with the CleverImage component. Let me show you how easy it is. The full code written in this post can be found at [GitHub Gist: CleverImg.astro](https://gist.github.com/11Firefox11/817beec030bc911bad8c39816cde6d1e).

## What is a responsive image?
Responsive images work well on all kinds of screen sizes and resolutions. It can help in optimizing page performance. For example, you don't need a 4k image on a mobile screen because it is just a waste.

It can be done in pure HTML. To create one, you have to modify an `img` element's attributes:
- [`srcset`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#srcset): specify multiple image sources and their scale factors (note that if `sizes` isn't defined then the browser will decide which to display, that is why order matters here, the browser will use the first usable image it finds)
- [`sizes`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#sizes): specify which image source to use from the `srcset` on which screen sizes.

These two attributes value generation will be implemented.

### Example
In the [previous part](https://tomoviktor.com/posts/thumbnail-generation) we created an automatic image generator, look at that for generating images, I am going to use an example related to that.

Let's say our base image is `cat.png`, it has different sizes and formats (file names scheme: `[NAME]-[WIDTH]-[HEIGHT].[EXTENSION]`): `cat-100-500.png`, `cat-100-500.webp`, `cat-200-1000.png`, `cat-200-1000.webp`. The smaller image should be loaded if the user's screen is smaller than 768px. WebP is preferred.
```html
<img 
  src="cat-200-1000.png"
  alt="a black cat with big green eyes"
  srcset="cat-100-500.webp 100w,
          cat-100-500.png 100w,
          cat-200-1000.webp 200w,
          cat-200-1000.png 200w"
  sizes="(max-width: 768px) 100px, 200px"
/>
```

For more explanation read [MDN - Responsive images](https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images).

## Implementation
### Setup
Create a new [Astro component](https://docs.astro.build/en/core-concepts/astro-components/), let's name it `CleverImg.astro`.

What [props]((https://docs.astro.build/en/core-concepts/astro-components/#component-props)) are needed?
- Basic `img` tag arguments: `imgPath` (`src`), `alt`, [`loading`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#loading) (if later want to add lazy load)
- `sizes` what tells the sizes to generate for `srcset`
- `breakpoints` that declares at what `maxWidth` which size source should be loaded, here `0` `maxWidth` will be the default non media query related size
- `withWebp` if the image(s) exist in `.webp` format too
```ts
interface Props {
  imgPath: string,
  alt: string,
  loading?: "eager" | "lazy" | null,
  sizes: { width: number; height: number }[],
  breakpoints?: {maxWidth: number, imgWidth: number}[]
  withWebp?: boolean,
}
const { imgPath, alt, sizes, withWebp, loading, breakpoints } = Astro.props; // init each prop as a const variable
```

Both `srcset` and `sizes` require separating strings with a comma. That is why both will be handled as an array and will be [joined](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/join) after.
```ts
const generatedSrcset: string[] = [];
const generatedSizes: string[] = [];
```

If you followed the [previous part](https://tomoviktor.com/posts/thumbnail-generation), and images are only generated on build, then put the following code into an if, so it only runs on build:
```ts
if (import.meta.env.PROD) {
  // ...
}
```

### Generate srcset
First, sort the `sizes`. Because order matters, order them ascending.
```ts
sizes.sort((a, b) => a.width - b.width);
```

Now, we have to append strings to `generatedSrcset` in format `[IMGPATH] [IMGWIDTH]w`. Path will be generated with function that was written in [previous part of this series]((https://tomoviktor.com/posts/thumbnail-generation)). This must be done for WebP format too.
```ts
for (const size of sizes) {
  ((withWebp ?? true) ? [true, false] : [false]).forEach((isWebp) => { // to do it for webp too
    generatedSrcset.push(`${ImageGenerator.generateName(imgPath, size, isWebp)} ${size.width}w`);
  });
}
```

There is a small modification which should be made. If there is a `breakpoint`, only include images which are in that. Because `breakpoint` will tell the browser to display which image and when, just those images are required. For this, let's filter the sizes and remove ones that aren't required.
```ts
let allBreakpointSizes: number[] | null = null;
if (breakpoints) {
  allBreakpointSizes = breakpoints.map(bp => bp.imgWidth);
}
sizes.sort((a, b) => a.width - b.width);
for (const size of sizes) {
  if (allBreakpointSizes && allBreakpointSizes.indexOf(size.width) === -1) {continue;} // skip if the curren width is not found in breakpoints
  ((withWebp ?? true) ? [false, true] : [false]).forEach((isWebp) => {
    generatedSrcset.push(`${ImageGenerator.generateName(imgPath, size, isWebp)} ${size.width}w`);
  });
}
```

### Generate sizes
Add strings to `generatedSizes` in `(max-width: [MAXWIDTH]px) {IMGWIDTH}px` format. There is one exception, if the `maxWidth` is `0` then it means that this is the default value.
```ts
if (breakpoints) {
  for (const breakpoint of breakpoints) {
    if (breakpoint.maxWidth === 0) {
      generatedSizes.push(`${breakpoint.imgWidth}px`); // handle default
    } else {
      generatedSizes.push(`(max-width: ${breakpoint.maxWidth}px) ${breakpoint.imgWidth}px`);
    }
  }
}
```

### Use it in HTML
The `src` should be the biggest sized image. If someone's browser doesn't support `srcset` then this will be used. The other attributes are nothing special, the generated arrays must be joined with comma.
```astro
---
// ...
const biggestSize = sizes.reduce((prev, current) => {
  return (prev.width > current.width) ? prev : current;
});
---
<img
  src={ImageCompressorIntegration.generateName(imgPath, biggestSize)}
  alt={alt}
  srcset={generatedSrcset.join(", ")}
  sizes={generatedSizes.join(", ")}
  loading={loading}
/>
```

If you followed the [previous part](https://tomoviktor.com/posts/thumbnail-generation), and images are only generated on build, then put the fully generated `img` in a conditional, and put a simple `img` in with `import.meta.env.DEV` condition. Look at the implementation at [GitHub Gist: CleverImg.astro](https://gist.github.com/11Firefox11/817beec030bc911bad8c39816cde6d1e#file-cleverimg-astro-L39-L49).