Blog - Sébastien Mouchet

How to optimize images for the Web

Posted on January 12, 2024, by Sébastien


By “optimizing”, I mean, making sure your images have a “reasonable” size.

Keeping your images small has benefits for users, for website owners, and even for other people:

The advice from this article mostly applies if you upload images to your own website.

If you upload them to social networks instead, they will get re-encoded, which means you should use the highest quality you have. Some image hosting services will also re-encode your images.

Vector images vs Raster images

There are 2 kinds of images: vector images, and raster images (bitmaps).

Raster images are the most common. Simply put, they’re rectangular matrices (grids) of pixels. For instance, all photos are raster images.
The following file formats all store raster images: JPEG, PNG, GIF, WebP, AVIF, TIFF, BMP, TGA, …

Vector images, on the other hand, are built by giving instructions to the computer about how to draw them. E.g. “draw a red circle here, a blue rectangle there, a Bézier curve, a gradient, …”.
Popular file formats for such images include AI (Adobe Illustrator), and SVG (Scalable Vector Graphics).
If you intend to use a vector image on the Web, you need to use the SVG format.

The huge advantage of vector images is that they can be scaled up without looking blurry or pixelated.

Vector images can be easily converted to raster images, but not the other way around.

Please note that, because SVG is a text-based format (XML), it compresses relatively well using gzip (or more advanced algorithms).
Therefore, you should enable compression at the web server level for SVG files.

Then, in order to determine whether converting your SVG to a raster image is worth it, you should compare the compressed size to the raster image version.

It’s often preferable to keep them in SVG format, especially for large images.

Image resolution

Display resolution has no impact on the file size of vector images, which can be scaled up and down arbitrarily.

On the other hand, pixel resolution is a crucial parameter for the size of raster images.

Therefore, you should figure out what size you need (in pixels) before exporting your image. For example, if you know it’s going to be displayed as a 640 x 480 image, don’t upload a 4000 x 3000 version…

Sometimes, responsive web design makes it impossible to know the final resolution in advance. In that case, you can actually specify multiple sizes in the “srcset” attribute of the <img> HTML element.

<img src="image-640x480.jpg"
     srcset="image-320x240.jpg 320w, image-640x480.jpg 640w"
     sizes="(max-width: 320px) 320px, 640px"

The “srcset” attribute also allows you to specify different images for high-density displays (Retina displays, etc).

<img src="image-320x240.jpg"
     srcset="image-640x480.jpg 2x"

Learn more about responsive images:

Lossy vs Lossless compression

Raster images can be compressed using either lossy formats or lossless formats.

As the name suggests, lossless encoding preserves all of the information, which guarantees that the exported image will look identical to what you see in your image editor.

Lossy compression discards some (hopefully not too visible) information, allowing for further space savings. It’s a tradeoff between file size and quality.

Please bear in mind that, even if you put the quality slider to 100%, lossy formats (such as JPEG) are never mathematically lossless.

By the way, quality sliders are arbitrary, and totally implementation-dependent. Do not expect the same level of quality between an 80% JPEG from Photoshop and one from GIMP, or between an 80% JPEG and an 80% WebP.

You should avoid re-encoding lossy images in a lossy format over and over, as it leads to generation loss (the quality degrades each time).

Photos should always use lossy formats, as they’re just too large when using lossless formats.

On the other hand, infographics and logos can often end up being smaller in PNG format (which is lossless) rather than in JPEG. Generally speaking, images with large areas of similar colors typically compress well using lossless formats.

Lossy: JPEG

JPEG is the most common lossy format. It’s been around since 1992, so it’s really ubiquitous and works in every browser.

If your image editor provides options, when exporting to JPEG:

Tip: Jpegcrop – a Windows application – allows you to change these options after the fact, without losing any quality, contrary to what you would expect when re-encoding a JPEG.
As the name suggests, it can do more than that, including cropping, rotating, and more, without suffering from generation loss.

Photos are usually encoded using 4:2:0 chroma subsampling, which saves some space, by using a lower resolution to store chroma information (color) than luma information (luminance).

More modern lossy formats: WebP and AVIF

Historically, lossy images were uploaded to the Web as JPEGs only, but new lossy formats have emerged, allowing for a smaller file size at the same quality level.

WebP was released in 2010, and was design to outperform JPEG.
As of early 2024, it now has pretty widespread support among browsers:

AVIF is even more recent, and generally offers a better compression ratio than WebP.
It is also increasingly supported (not yet as much as WebP):

Therefore, serving WebP or AVIF to supported browsers – instead of JPEG – can save some bandwidth.

I highly recommend setting up a fallback to JPEG, in case the browser only supports JPEG, or in case you haven’t uploaded WebP/AVIF versions.

For instance, here’s the Nginx configuration I use on the blog, which detects browser support via the “Accept” HTTP header:

# Check whether the client can handle WebP images
map $http_accept $webp_suffix {
    default   "";
    "~*webp"  ".webp";

# Check whether the client can handle AVIF images
map $http_accept $avif_suffix {
    default   "";
    "~*avif"  ".avif";
    # Fallback to WebP if the client supports WebP but not AVIF
    "~*webp"  ".webp";

server {
    # ...

    location ~* \.(?:jpg|jpeg)$ {
        add_header Vary Accept;
        # When requesting image.jpg,
        # try image.jpg.avif and image.jpg.webp first
        try_files $uri$avif_suffix $uri$webp_suffix $uri =404;

Again, to avoid generation loss, always encode your WebP and AVIF images from the highest quality you have, not from your already optimized JPEGs.

Lossless: PNG

PNG is the main lossless format of the Web.

I recommend processing your PNG files with an optimization tool such as OptiPNG, which can recompress them to a smaller size, without losing any information:

Another potentially useful program is “pngquant”:

“pngquant” works differently, and is actually not lossless: it converts PNG images to a 256-color version using an optimized palette.
This trick can sometimes reduce file size significantly, and is not too visible on images with a few hundred unique colors (perhaps up to 1,000 or 2,000).

Both are command-line tools, but are quite easy to use.

More modern lossless format: WebP

In addition to being an alternative to JPEG, WebP also provides a lossless mode, which results in smaller files than PNG.

Again, you can serve WebP to supported browsers, and fallback to PNG otherwise.

AVIF also features a lossless mode, but it’s not as optimized as WebP, and is therefore not worth using (all browsers that support AVIF also support WebP).

File size comparisons

To give you a practical example, let’s consider the 2 images I’ve embedded in the following blog post:

The chart (680 x 508) was:

As you can see, PNG and lossless WebP are totally usable for charts and infographics, and they can sometimes benefit from a 256-color palette.

The photo was initially a 4032 x 3024 JPEG with a file size of 3.6 MiB – way too big.

After scaling it down to 640 x 480, it comes out at:

As mentioned earlier, lossless formats should not be used for photos.

Animated images: GIF

Animated images are a special category of images, which are not supported by formats such as JPEG.

Animated PNG exists (APNG), but the de-facto standard format for animated images is GIF (pronounce it however you want 😜).

Know that GIF is a very inefficient format (in terms of compression), and is furthermore limited to only 256 colors.

It’s fine for small loading icons, for instance, but, please, avoid using it for large images, or video content.

If your GIF image comes out at several megabytes (MB), you should probably use an actual video format. Animated pictures have to encode every frame separately, whereas video codecs can employ more sophisticated techniques, enabling a significant file size reduction.

More modern formats: WebP and AVIF

WebP and AVIF also have animated versions, which are superior to GIF in every way.
You should use them, again, while providing a fallback to GIF.


Reducing the file size isn’t the only way to save bandwidth:
You should definitely set up a proper caching strategy.

For this blog, I use an very simple – yet extremely efficient – caching strategy:

1. Images are stored in browser cache for up to a year.
Here’s the corresponding Nginx config:

add_header Cache-Control "public, max-age=31536000";
add_header Cache-Control immutable;

Once an image is cached by the user’s browser, no further requests will be made for that image.

If I absolutely need to update the content of an image, I will update its URL in the HTML source code – for instance, by adding a query string parameter (e.g. “?version=2”). This is known as a cache busting technique.

This caching strategy can actually be used for all assets (CSS, JavaScript, …), not just images.

2. HTML documents require revalidation, so that the browser does not use an outdated cached copy:

add_header Cache-Control no-cache;
# Equivalent to the older syntax:
#add_header Cache-Control max-age=0, must-revalidate;

Don’t let the “no-cache” keyword fool you: it is not equivalent to “no-store”.
It means that, the browser will check for updates, and if the HTML document hasn’t been modified more recently than the cached copy, the server will respond with a “304 Not Modified” response code, and an empty response body.

Content delivery network

Websites with a lot of traffic can benefit from a content delivery network (CDN).

CDNs consist of geographically distributed networks of proxy servers.

This is useful when the traffic becomes too much for a single server to handle, and also speeds up image loading for users (latency and bitrate), due to their worldwide presence.

Famous CDNs include Akamai, Cloudflare, Fastly and Amazon Cloudfront.

Additionally, some companies offer image optimization services built on top of CDNs.
Examples include Cloudinary (uses Akamai) and Imgix (uses Fastly).
If you decide to use such a service, make sure to feed them the highest quality source images you can, and to tune the encoding quality to your needs, or the output images could look ugly.

Lazy loading

One last trick you can use, to save some bandwidth, is to set the “loading” attribute of your <img> elements to “lazy”:

<img loading="lazy" src="image.jpg" alt="..." />

This is used to defer loading of off-screen images until they are scrolled into view.

Of course, this only saves any bandwidth if said images are initially off-screen, and if some of your users never scroll.

And seeing images pop into view as you scroll can be slightly annoying.