Responsive, lazy-loaded, jank-free images on the web

Ryandi Tjia 18 May 2020

There are 3 aspects on how you can improve the loading of images on your webpage. This post walks through each briefly, and serves as basic for the next series of posts where we’ll go about implementing it.

#The responsiveness (aka load images of appropriate size)

We can tell the browser which image is optimal for it to load, via these 2 attributes:

Step 1: srcset attribute

Example: <img srcset="dog-800x400.jpg 800w, dog-3840x1920.jpg 3840w" … />

Tells the browser what choices it has, distinguished by widths. A mobile browser probably doesn’t want to load the image that’s 3840 pixels wide.

Example for terseness and to show extremes, in practice you’d want more images in between. Learn more at MDN.

Step 2: sizes attribute

Example: <img sizes="(min-width: 768px) 50vw, 100vw" … />

Hints the browser how to choose an image by telling it how it should expect the image to appear on screen. In this case full width (100vw) by default, but half width (e.g. 2 images side-by-side) on a tablet or wider.

Has no effect on the actual layout of the image on the page.

Personally this goes into micro-optimization territory, and a chore to write. If you don’t specify, the default value is 100vw. The library lazysizes helps automate this sizes attribute, in addition to lazy-loading — which is our next section.

#The lazy-loading (aka load images as needed)

There are plenty of ways to achieve this, with differing levels of browser support.

Option 1: loading attribute

Example: <img loading="lazy" … />

Probably the cleanest way, but browser support as of time of writing isn’t great.

Option 2: third-party libraries

Google Developers site has a list of recommendations.

Option 3: your own implementation

The modern way is to use IntersectionObserver, but it won’t be covered in this post.

#Jank-free layout by specifying aspect-ratio

You’re reading text on a webpage, and an image loads and pushes the text to the bottom of your screen. This happens because the browser doesn’t know the height of the image until the image starts loading.

Option 1: using width and height attributes

Watch the following video to see the problem in action as well as a fix already in Firefox:

The support in other browsers is currently unknown to me.

Option 2: using aspect-ratio placeholder

The following code makes use of the “padding hack” to hold an element (empty or otherwise) in place.

	.wrapper {
		position: relative;
		overflow: hidden;

	.aspect-ratio-placeholder {
		/* 16-by-9 aspect-ratio */

		/* Percentage comes from: 100% ÷ (16 ÷ 9) */
		padding-bottom: 56.25%;

		/* Alternative */
		padding-bottom: calc(100% * 9 / 16);

	img {
		position: absolute;
		top: 0;
		left: 0;
		width: 100%;
		height: 100%;

<div style="position: relative; overflow: hidden">
	<div class="aspect-ratio-placeholder"></div>

	<img src="actual-image.jpg" … />

Option 3: using low-quality image placeholder (LQIP)

Building from option 2, you can show a blurred version of the image instead of an empty element. A ‘base64-encoded, 20×20 pixels’ version of the image is often used for this.

Incomplete example: <img src="data:image/jpeg;base64,/9j/2wBDAAYEBQYFB…truncated" … />

GIF of completed example in action:

LQIP demo

#Next up

This post is a set up for the next series of posts where we’ll be putting all three aspects together. If you’re using Svelte with Headless CMSes, you don’t want to miss it.