Performant Images on the Web

published: / updated:

Back in 2001 when I started building websites, the Internet was very, very slow – so, when you wanted to show images, you had to make them really small, and compress them a lot, or the user would have to wait for a long time. I learned a lot about efficient image delivery back then, and a those things are still true. But we also have some newer tools to help us making websites fast. Here are some of the things I learned in the last 20+ years; some of them basics, some of them very niche:

When in Doubt, Use jpeg

The best format when you don't want to think about is, is definetly .jpg. So, just use .jpg for your files.

If you have illustrations, especially with a low color count, you could also use .png - this results in less artefacts, higher quality, and (depending on the content) a smaller file size. Another alternative, for vector files, is .svg, but make sure to include width and height attributes, because sometimes the dimensions of a svg are not defined and the browser has no idea how large the image is.

When using jpg, use a compression rate of 50% - 80% (where 100% is lossless, and 0% is very very small and very very ugly). The compression rate depends on the image, the dimensions and the use case, you may need to experiment a bit. A good starting point is 65%. Generally said, images with larger dimensions can be compressed more than images with smaller dimensions.

When using .png, you can reduce the number of colors or set the colorpalette to greyscale to save some space. When you need transparency though, use PNG-24, because the color-reduced PNG-8 only supports full transparency or no transparency, no values between (like GIF).

Always Add width and height Attributes

You should always add width="" and height=" attributes to the <img> tag, even if the image gets scaled to another size (via css or inline styles), because then the browser knows the original size of the image before it is loaded, and can lay out the space the image will occupy, once it is loaded. This helps imensly with preventing layout shifts.

<img src="my-beautiful-image.jpg" width="600" height="400" alt="This is a beautiful image">

You can specify the dimensions you want the image to have via CSS. I mostly use something like this:

img {
	max-width: 100%;
	height: auto;
}

This makes sure, that images don't exceed the viewport (or container) they are in (max-width: 100%), and also makes sure to not stretch images (height: auto). Without setting the height to auto, images may get stretched or squashed, because then the height ist set to a fixed pixel size.

Make sure to save your images in the resolution they are (likely) to be displayed. Don't include a 1000px wide image, if it gets only displayed in a 300px wide div; just resize it to 300px wide for this case. Your images should be 2500px wide and 2000px high at a maximum, even if they get displayed larger, because if you save them at a higher resolution, the filesize gets way too big. You should target 500 KB at max per image (depending on the number of images on your site you can get away with a larger filesize, or need to go lower if there are many images. A good rule is to not exceed 10 MB per page. The lower, the better.)

srcset for Higher Details

Some screens may have a higher pixel density (high-dpi/"retina" displays). You can add additional images size for those screens, to load a higher resolution if needed, and fall back to the default resolution when the user is on a "normal" screen.

<img srcset="image_800x600.jpg 1x, image_1600x1200.jpg 2x, image_2400x1800.jpg 3x" src="image_800x600.jpg" width="800" height="600">

This will load the image_800x600.jpg by default. But if you are on a high-dpi display, it may load the image_1600x1200.jpg or image_2400x1800.jpg instead, depending on the pixel density of the target display. See mdn web docs for details.

srcset for Responsive Images

You can use <source> elements inside the <picture> element to provide different images for different resolutions:

<picture>
	<source media="(max-width: 600px)" srcset="image_400x300.jpg 1x, image_800x600.jpg 2x" type="image/jpeg">
	<source media="(min-width: 600px)" srcset="image_800x600.jpg 1x, image_1600x1200.jpg 2x" type="image/jpeg">
	<img src="image_800x600.jpg" width="800" height="600">
</picture>

This will load images with smaller dimensions for screens that have a smaller width than 600px, and bigger images for larger screens.

srcset and sizes

If you want to tell the browser, which size the image will be, so that the browser can pick the best size from the srcset even before the layout step finishes, you can add the sizes attribute:

<img sizes="(max-width: 820px) 100vw, 800px" srcset="image_480x360.jpg 480w, image_800x600.jpg 800w, image_1600x1200.jpg 1600w" src="image_800x600.jpg" width="800" height="600">

This tells the browser, that the image on the screen will have a width of 800px, unless the viewport has a maximum width of 820px, then the image will have a width of 100% of the viewportwidth. The browser can then use the srcset to determine, which image is best for the current viewport and pixel density. This can happen, before the CSS is loaded, and the browser can start (pre-)loading the correct image immediatley, as soon as the HTML is loaded. You can read more about this on mdn.

There will be a sizes="auto" option in the future, which is usefull if you want to lazy-load the image anyway, so the loading can happen after the layout step, and you don't want to add all possible image sizes in the sizes attribute. But the browser support isn't there yet.

Lazy-Load Images

You should add the loading="lazy" attribute to the <img> tag. With this tag, the image will not be loaded when the page loads, if it is not currently in the viewport. The browser will automatically load the images below the viewport, once the users starts scrolling and gets near the image. If the user never scrolls, the image never has to load.

Don't Lazy-Load Images

Images that are in the initial viewport, should not be lazy loaded, so you should omit the loading attribute or set it to loading="eager". In that case, the browser starts preloading these images, before it even starts laying out the page, which helps with the initial page load and layout shifts. Because you don't know the size of the viewport of your visitor in advance (they may be on a mobile phone or a gigantic desktop display), use your best judgement. For example, on this website, the first two images are not lazy loaded, the rest is.

If your website targets very lossy or slow connections, omiting the loading attribute on every image could also help, because then all images load one after the other and are already there, when the user scrolls down. When you are on a lossy connection, you may open a website and then wait until everything is loaded, before starting to scroll down. If images are set to lazy load, this may be very annoying. This is a edge case though, so when in doubt set images that are below the initial viewport to lazy load.

Additional File Formats: avif, webp

You can use modern file formats, to make the filesize smaller or the quality higher. AVIF and WEBP are a good choice for this. Some browsers may only support one of the two, or none, so you should always provide a jpg (or png) fallback for now.

<picture>
	<source srcset="image_800x600.avif 1x, image_1600x1200.avif 2x, image_2400x1800.avif 3x" type="image/avif">
	<source srcset="image_800x600.webp 1x, image_1600x1200.webp 2x, image_2400x1800.webp 3x" type="image/webp">
	<source srcset="image_800x600.jpg 1x, image_1600x1200.jpg 2x, image_2400x1800.jpg 3x" type="image/jpeg">
	<img src="image_800x600.jpg" width="800" height="600">
</picture>

This will load the image in avif or webp if the browser supports it, and if not falls back to the jpg. It also chooses the correct resolution depending on the pixel density of the display.

AVIF-support seams to be good enough now that you may ommit webp, and just use a jpg fallback, this saves on webspace and conversion time.

Compress Your Images, then Compress Some More

After you saved your images, you can use pngcrush, tinyjpg or other tools to compress them some more. This will save 10% - 20% of filesize, without loosing any quality. Sometimes, using those tools more than once on an image may give you additional compression rate. You could also automate this process with a small bash script or something like this.

Tell the Browser Which Images to Fetch First

You can prioritize the images you want to load faster with a higher fetchpriority.

<img src="not-important.jpg" fetchpriority="low">
<img src="very-important.jpg" fetchpriority="high">

This will load the very-important.jpg before loading the not-important.jpg, even though the order in the HTML is the other way around. This is not supported in all browsers yet, and is marked as experimental, so the syntax may change in the future. See the mdn web docs for details. You only need this for very special use cases, so by default just omit the fetchpriority tag.

Set the Decode Priority

You can tell the browser, that it's ok to decode an image slower, if it is not important, or decode it with a high priority, if it is the most important thing on your website and needs to be there as fast as possible:

<img src="very-important.jpg" decoding="sync">
<img src="not-important.jpg" decoding="async">

Using decoding="async" will display the image with a lower priority, and more performance is available for other tasks, like layout or JavaScript execution. You only need this for very special use cases, so by default just omit the decoding tag. Or you could add decoding="async" to every image that also has loading="lazy", and add decoding="sync" to every image that has loading="eager".

Color Preview

You could use the average color of an image as the background color for the image, so that while the image gets (lazy-)loaded, this color is visible and is then replaced by the image. There are some online tools that can get the average color, or you could get the average color with php like this:

<?php
function get_average_color( $imagepath ) {
 
	$default_color = ['r' => 0, 'g' => 0, 'b' => 0];
 
	$imagesize = getimagesize( $imagepath );
 
	$image = false;
	if( $imagesize[2] == IMG_JPG ) {
		$image = imagecreatefromjpeg( $imagepath );
	} elseif( $imagesize[2] == IMG_PNG ) {
		$image = imagecreatefrompng( $imagepath );
	}
 
	if( ! $image ) return $default_color;
 
	$imagewidth = $imagesize[0];
	$imageheight = $imagesize[1];
 
	$small = imagecreatetruecolor(1,1);
	imagecopyresampled( $small, $image, 0,0,0,0,1,1, $width, $height );
 
	$rgb = imagecolorat($small,0,0);
	$main_color['r'] = ($rgb >> 16) & 0xFF;
	$main_color['g'] = ($rgb >> 8) & 0xFF;
	$main_color['b'] = $rgb & 0xFF;
 
	// cache $main_color in a database or textfile or something, because it may be costly to do this for every image on every page load!
 
	return $main_color;
}

You can then use these r/g/b values as a background color:

<?php
$imagepath = 'my_beautiful_image.jpg';
$main_color = get_average_color( $imagepath );
?>
<picture style="background-color: rgb(<?= $main_color['r'] ?>, <?= $main_color['g'] ?>, <?= $main_color['b'] ?>);">
	<img src="<?= $imagepath ?>" loading="lazy">
</picture>

base64-encoded Preview Image

You can also use a very small and blurry image, encoded as base64, directly in your HTML.

First, resize your image to very small dimensions, like 100px wide, and add some blur on top. Save it as .jpg, then base64 encode it (via PHP, or search for jpg to base64). This base64 encoded string can be added as a background image:

<?php
$imagepath = 'the_best_image_in_the_world.jpg';
$base64_encoded_preview = 'data:image/jpg;base64,'.base64_encode(file_get_contents($imagepath));
?>
<picture style="background-image: url(<?= $base64_encoded_preview ?>);">
	<img src="<?= $imagepath ?>" loading="lazy">
</picture>

Because the preview-image is base64-encoded and embedded directly in the HTML, it is loaded with the HTML and already there as soon as the browsers renders the <picture> element. The <img> will then get lazy-loaded, and replaces/overlays the blurry preview image.

gif is the Worst File Format

The gif file format is very old and inefficient. If the image does not move, use .png or .svg (for vector files) instead, which provide a much smaller file size and higher quality.

If you want to use animated gifs, convert them to mp4 instead. You can use Handbrake or ffmpeg for this. The video should be converted to the h.264 codec in a mp4 (or m4v) container. h.265/hevc may also be possible, but is not supported by all browsers, so you should also add a h.264 fallback. The video dimensions should match the dimensions on the page, so that the video does not need to be scaled, but should not exceed 1920×1080px. The resulting filesize should not exceed 3 MB.

<video src="my_beautiful_gif.m4v" width="400" height="400" poster="my_beautiful_gif.jpg" preload="metadata" loading="lazy" autoplay muted loop playsinline disablepictureinpicture disableremoteplayback>

This will automatically play the video in a loop, without sound and without controls. See the mdn web docs for details about all those video attributes.

Don't Trust Me. Test Your Website

You should always test your website to see what helps. You can use the Developer Tools of your browser to view a waterfall diagram of what gets loaded when. You can also disable the browser cache, to force a reload of all images, and use throttling to simulate slower connections.


Have a comment? Drop me an email!
This helped you? Consider buying me a ♥ coffee ♥