Adding responsive and efficient images to your blog

It’s been a while since I was up to date on image formats for the web. Back then in the 90s, I actually read the original JPEG paper in order to understand how it worked, because I was preparing a presentation to my study group on the MPEG paper and needed to understand how I-frames were encoded. At the time, my main computer was an Amiga 500 and I spent countless hours writing an IFF/ILBM codec for libjpeg so I could transcode JPEG images into the Amiga native image format. Decoding a single JPEG image for display on my Amiga at 368×567 pixels took many minutes. Fun times!
Back to 2025. A lot has happened in the land of image formats, and after stumbling on Lighthouse and its various audits, I learned that my blog was way behind in terms of image performance. So I decided to catch up and modernize my blog image formats.
The problem: images are huge and slow
My blog’s Lighthouse performance score was… not great. Massive JPEG files took too long (for Lighthouse, at least) to load, especially on mobile. I needed a modern solution that would deliver great images without the bandwidth penalty.
Modern image formats: the new kids on the block
Here’s what I learned about the current state of image formats:
AV1 Image Format (AVIF): AVIF uses the AV1 video format’s codec to encode/decode still images or image sequences. Just like JPEG uses similar techniques to encode still images as MPEG does for video, AVIF is the still image version of AV1.
WebP: Invented by Google, WebP intends to replace all older formats like JPEG, PNG and GIF at once, by offering all of their features like lossy and lossless compression, transparency, and animation in one format to rule them all. WebP is related to the VP8 video format. There’s that still/moving image relationship again.
Browser support: we’re in good shape
According to Can I Use:
- AVIF is supported by 93.63% of browsers that are used globally (Chrome 85+, Firefox 93+, Safari 16.1+).
- WebP: Supported by 95.69 of browsers used globally
Practically universal support for both at this point.
Plus, there’s always a fallback to JPEG strategy available that handles older browsers gracefully, so there’s really no downside to implementing this today.
Performance: looking at the data
According to Netflix’s research and my own blog’s data:
- AVIF: ~50% smaller files than JPEG
- WebP: ~30% smaller files than JPEG
To put this in perspective, here are the actual file sizes for this article’s header image at 800px width:
- Original JPEG: 52,227 bytes
- AVIF: 27,953 bytes (46% savings!)
- WebP: 26,344 bytes (50% savings!)
The results are even better than the industry averages suggest! Both modern formats deliver significant savings over traditional JPEG.
Pro tip: writing this article also fixed a subtle performance bug in my setup: Turns out that Zola uses quality=lossless
by default for WebP, as opposed to lossy (but sensible) quality levels for AVIF (70) and JPEG (75). This was giving me much bigger files (330,020 bytes for WebP vs. the original 52,227 bytes for JPEG)! Now, with proper lossy encoding, every browser gets optimized images regardless of which modern format they support.
The solution: responsive images with multiple formats
The best strategy is to serve AVIF first, WebP second, and JPEG as a fallback. But how can this be automated? Enter the HTML <picture>
element with <source>
tags.
What you’ll need
Before diving in, make sure you have:
- A static site generator that supports image processing (I like Zola)
- Willingness to accept roughly 3x storage usage (since you’re generating multiple formats)
- Patience during builds (processing takes longer, but it’s worth it)
Implementation: building the perfect image macro
Zola supports image transcoding and resizing through the resize_image() function as part of its Tera templating engine. This means you can keep using JPEG or PNG for adding pictures to posts, and let Zola transcode them into the right formats and sizes for publishing! 🎉
Start simple: basic multi-format support
Here’s a simplified version that demonstrates the core concept:
<picture>
<!-- AVIF: best compression -->
<source srcset="image.avif" type="image/avif">
<!-- WebP: good compression, wide support -->
<source srcset="image.webp" type="image/webp">
<!-- JPEG: universal fallback -->
<img src="image.jpg" alt="Description" loading="lazy">
</picture>
The full solution: responsive + multi-format – pick two
Now for the complete macro that handles multiple sizes and multiple formats, courtesy of Claude 4 Sonnet:
{% macro responsive_image(image, alt="", caption="", class="", sizes="(max-width: 768px) 100vw, (max-width: 1024px) 80vw, 1200px", lazy=true) %}
{% set avif_400 = resize_image(path=image, width=400, op="fit_width", format="avif") %}
{% set avif_800 = resize_image(path=image, width=800, op="fit_width", format="avif") %}
{% set avif_1200 = resize_image(path=image, width=1200, op="fit_width", format="avif") %}
{% set webp_400 = resize_image(path=image, width=400, op="fit_width", format="webp", quality=75) %}
{% set webp_800 = resize_image(path=image, width=800, op="fit_width", format="webp", quality=75) %}
{% set webp_1200 = resize_image(path=image, width=1200, op="fit_width", format="webp", quality=75) %}
{% set jpg_400 = resize_image(path=image, width=400, op="fit_width", format="jpg") %}
{% set jpg_800 = resize_image(path=image, width=800, op="fit_width", format="jpg") %}
{% set jpg_1200 = resize_image(path=image, width=1200, op="fit_width", format="jpg") %}
<picture class="{{ class }}">
{# AVIF sources - best compression #}
<source
srcset="{{ url_helpers::make_relative(url=avif_400.url) | safe }} 400w, {{ url_helpers::make_relative(url=avif_800.url) | safe }} 800w, {{ url_helpers::make_relative(url=avif_1200.url) | safe }} 1200w"
sizes="{{ sizes }}"
type="image/avif">
{# WebP sources - good compression, wide support #}
<source
srcset="{{ url_helpers::make_relative(url=webp_400.url) | safe }} 400w, {{ url_helpers::make_relative(url=webp_800.url) | safe }} 800w, {{ url_helpers::make_relative(url=webp_1200.url) | safe }} 1200w"
sizes="{{ sizes }}"
type="image/webp">
{# JPEG fallback with responsive sizing #}
<img
srcset="{{ url_helpers::make_relative(url=jpg_400.url) | safe }} 400w, {{ url_helpers::make_relative(url=jpg_800.url) | safe }} 800w, {{ url_helpers::make_relative(url=jpg_1200.url) | safe }} 1200w"
sizes="{{ sizes }}"
src="{{ url_helpers::make_relative(url=jpg_800.url) | safe }}"
alt="{{ alt }}"
width="{{ jpg_800.width }}"
height="{{ jpg_800.height }}"
class="rounded-lg shadow-md"{% if lazy %}
loading="lazy"{% endif %}>
</picture>
{% if caption %}
<figcaption class="mt-2 text-sm text-gray-600">
{{ caption | safe }}
</figcaption>
{% endif %}
{% endmacro %}
Understanding the Magic
Let me break down what’s happening here:
The srcset
attribute tells the browser: “Here are multiple versions of this image at different widths.” The browser picks the most appropriate one based on the device’s pixel density and viewport size.
The sizes
attribute gives the browser hints about how large the image will be displayed. For example, (max-width: 768px) 100vw
means “on screens 768px wide or smaller, this image will take up 100% of the viewport width.”
The type
attribute on <source>
elements lets the browser skip formats it doesn’t support and jump straight to the fallback.
Performance Bonuses
This macro includes several performance optimizations:
- Automatic dimensions: the
width
andheight
attributes prevent layout shifts as images load, and Zola’sresize_image()
gives us the values for free - Configurable lazy loading: on by default, but you can disable it for above-the-fold images
- Relative URLs: The
make_relative()
helper strips site prefixes when needed
The results: Lighthouse love
After implementing this system, my Lighthouse performance score jumped to 100! But more importantly, images load noticeably faster, especially on mobile connections.
So there you have it! Modern image formats, responsive sizing, and automatic format selection. Your users get faster loading times, your server saves bandwidth, and Lighthouse gives you a sweet 100 score. What’s not to love?
The web has come a long way since my Amiga days, and it’s exciting to see how much better we can make the browsing experience with just a bit of technical finesse. Now go forth and optimize those images! 🚀