Skip to main content
Eystein codes

Super fast responsive image loading in Eleventy

One of my goals is to make this website more sustainable. One part of doing that is to make it as lightweight as possible. Making it greener, with a smaller carbon footprint, loading faster on all connections and devices, lower server costs. Win win all around!

I don't have any javascript, so it's the images that are the main reason for large and slow loads here.

To optimise them I needed to transcode the images into modern file formats that can compress images into much smaller sizes. But I don't want to do this manually, so an automatisation is necessary.

I also want to serve the right size for further optimision. And to have a fallback for older browsers.

That gives me 5 areas to consider:

  1. Transcode input images to avif and webp.
  2. Responsive sizing
  3. Nunjucks and HTML markup
  4. Lazy loading

Transcode input images to avif and webp. #

My website is based on the Eleventy base-blog, which uses the 11ty image plugin. (Note that the Eleventy config settings in the base-blog template which I am using is slightly different from the examples in the 11ty image docs.)

Using the default image plugin config from the base-blog, I use Nunjucks shortcode in the markdown file. Something I didn't even know I could do!

The shortcode looks like this:


// ./content/blog/blogpost-title/blogpost-title.md

{% image "/blog/blogpost-title/image.jpg", "A demo image." %}
  1. The first part is the name of the shortcode: image .
  2. Then the original filename in quotes.
  3. And the text for the alt attribute value in quotes.

The images need to be in the same folder as the markdown file, so I organised my posts into sub-folders so I could just drop the original image files in that same folder. (This can be customized in eleventy config for the image shortcode. )

My file structure then looks like this:

./content/blog/blogpost-title/blogpost-title.md
./content/blog/blogpost-title/image.jpg

That creates the following HTML:

<picture>
	<source type="image/avif" srcset="/img/xyz-748.avif 748w">
	<source type="image/webp" srcset="/img/xyz-748.webp 748w">
	<img alt="A demo image." loading="lazy" decoding="async" src="/img/xyz-748.png" width="748" height="276">
</picture>

The image plugin has made 3 copies of my original file, in AVIF, webP, and the original file format. Those are placed in a /img folder in the root of the project.

The plugin outputs HTML for the responsive image using the <picture> element. With its child element - <source> - I can set up AVIF and webP as source files. Then the browser does the rest of the job for us: loading the AVIF format if it supports it. If it doesn't, it tries the webP format next, and finally the original file format in an <img> tag as a fallback.

Since all the images are in the same folder, they are assigned random names to avoid conflict. (And yes, this can also be customized in config.)

File path! #

For the image I need to include the full file path within my ./content/ folder since the image shortcode can be used other places than just my blog.

Resize to appropriate sizes #

In the example above I didn't specify any sizes. The image plugin creates default sizes based on the original image size.

On my site I use the same image both as a banner and a thumbnail, so those need very different sizing.

Two additional (optional) properties to the image shortcode helps with that: widths and sizes.

The first value in the widths array (WidthA) belongs to the first value in sizes (SizeA), and the "WidthB" to the "SizeB".


{% image "/path/image-file.jpg", "Alt text here", [WidthA, WidthB], "SizeA, SizeB"  %}

Don't forget to keep the properties in the shortcode comma-separated.

Here I've added real numbers for my banner and thumbnail sizes. The original image file has a width of 748px, so that's what I'm using for the full-width banner size. If the original was 1200px wide, I'd use that number instead.


{% image "/blog/use-alt-attribute/les-horribles-cernettes.jpg", "Four women wearing 1990s gala dresses.", [480, 748], "(max-width: 480px) 480px, 100vw" %}

This tells the browser to use the 480px wide image when the image is rendered at less than 480px (like a CSS container size query, rather than a viewport size media-query). And to use the 748px image at full width at all sizes above that.

The HTML output for this step looks like the following:

<picture>
	<source
		type="image/avif"
		srcset="/img/xyz-480.avif 480w, /img/xyz-748.avif 748w"
		sizes="(max-width: 480px) 480px, 100vw">
	<source
		type="image/webp"
		srcset="/img/xyz-480.webp 480w, /img/xyz-748.webp 748w"
		sizes="(max-width: 480px) 480px, 100vw">
	<source
		type="image/jpeg"
		srcset="/img/xyz-480.jpeg 480w, /img/xyz-748.jpeg 748w"
		sizes="(max-width: 480px) 480px, 100vw">
	<img
		src="/img/xyz-480.jpeg"
		alt="Four women wearing 1990s gala dresses."
		width="748"
		height="595">
</picture>

Lazy loading #

The default for the image plugin is lazy-loading, which is good for faster downloads. But not for the banner images, or any other images that will be visible before the visitor scrolls down.

I added a 4th property - loading - in addition to "alt", "widths" and "sizes" in the image config in eleventyConfig. Now I can pass a true/false for lazy-loading.

My full image shortcode configuration in Eleventy Config:

eleventyConfig.addAsyncShortcode("image", async function imageShortcode(src, alt, widths, sizes, loading) { // <- New property past in
	let formats = ["avif", "webp", "auto"];
	let file = relativeToInputPath(this.page.inputPath, src);
	let metadata = await eleventyImage(file, {
		widths: widths || ["auto"],
		formats,
		outputDir:
			path.join(eleventyConfig.dir.output, "img"), // Advanced usage note: `eleventyConfig.dir` works here because we’re using addPlugin.
	});

	let imageAttributes = {
		alt,
		sizes,
		loading: loading || "lazy", // <- Check for new property
		decoding: "async",
	};

	return eleventyImage.generateHTML(metadata, imageAttributes);
});

The new loading: loading || "lazy" property checks the "loading" property for a variable, received from the Nunjucks image shortcode. If no value, it falls back to the default, which is lazy-load.

The opposite of loading="lazy" is loading="eager", so I'm using that.


{% image "/blog/use-alt-attribute/les-horribles-cernettes.jpg", "Four women wearing 1990s gala dresses.", [480, 748], "(max-width: 480px) 480px, 100vw", "eager" %}

Now I get the following HTML. It's exactly the same, except the loading attribute is set to "eager". This way I can leave out the last attribute when adding "regular" images that lazy-load.

<picture>
	<source type="image/avif" srcset="/img/foYRv2RPJx-256.avif 256w">
	<source type="image/webp" srcset="/img/foYRv2RPJx-256.webp 256w">
	<img alt="A grey rectangle" loading="eager" decoding="async" src="/img/foYRv2RPJx-256.png" width="256" height="176">
</picture>

Conclusion #

Replacing the images on my website boosted the Lighthouse score right up to all 100s, it completes loading at around the 1 second mark, making it faster than 99% of the web. Thus creating an equally smaller carbon footprint through the use of server space, data transfer and client power.