DEV Community

Cover image for Web Images: Resize and Convert Perfectly (and Automatically)
Tom J.
Tom J.

Posted on • Originally published at tomj.pro

Web Images: Resize and Convert Perfectly (and Automatically)

Pretty much every front-end developer knows that we need to deliver the smallest possible images to users without affecting their quality. We all know how to achieve that. But it’s a chore nobody likes doing. Also from a business standpoint, it takes time, and time is money. So, "good enough" is just good enough.

Let me share how we've improved and automated perfect image delivery without creating more work for developers.

How It's Usually Done

Maybe it's not you, but it's many—probably most of us.

You export a picture in PNG at 2× the intended size to cater to high-density screens and use it in an <img> tag. If you're willing to spend an extra 30 seconds (or more), you do better: you convert it into WebP and place both versions in a <picture> element, letting the browser select the best one (well, not the best one, just the newest supported and to browser favorite format).

This is the "good enough," and it often really is.

But it's not perfect. New iPads are large and can utilize 2.5× or even 3× images. On the other hand, a standard corporate Lenovo ThinkPad doesn't need that extra detail, and a 1× image is perfect.

And honestly, all that is fine for manual work. Nobody can spend 15 minutes on a single picture.

Automating to Perfection

For my perfectionist brain, "good enough" isn't what the phrase says. Also, some of our clients are in highly competitive fields, so we started researching options. It didn't take long. We already knew that browsers send HTTP headers indicating the image formats they support.

What we needed was to find out that the <picture> element can load images based on the pixel density of a given screen. That's something you can hardly do with server-side rendering, and adjusting src via JavaScript is out of the question for multitude of reasons.

With this, we have all we need:

  • Formats the server can serve.
  • Dimensions of the image requested by the front-end developer.
  • Pixel density of the display.

The Process

Here's how we automated the image optimization process:

  1. Accept Any Image Upload

    We let developers and admins upload and save whatever picture they want (of course developers are more careful, need to be). Our system can handle anything - even that 250 MB JPEG straight from a DSLR camera that we managed to successfully convert and resize and then burst in laugh when we saw the logs.

  2. Automated Conversion and Compression

Once an image is uploaded, our system automatically:

  • Converts it to multiple formats: PNG, WebP, and AVIF.
  • Compresses each version using a 90% quality setting.

Why 90%? Because the last 10% of quality often results in highly diminishing returns. You save significant storage space and bandwidth without any visible difference in visual quality.

  1. Generate Multiple Resolutions

    For each image, we generate multiple sizes based on pixel density multipliers:

  • 1.5×
  • This ensures that devices with high-resolution screens get sharp images, while others receive appropriately sized images.

    And for future-proofing, we just add or change numbers and all is automatic to handle (we don’t do VR / AR content just yet, but I suspect that’s the tech where it’ll come in handy).

  1. Dynamic Image Serving

    In our HTML templates, we specify the desired image dimensions (width or height). Our server-side code then:

  • Checks if the optimized images exist.
  • If not, queues them for processing.
  • Generates the appropriate <picture> element with srcset for pixel densities.

    The browser automatically selects the best image based on the device's capabilities.

The result looks like this:

<picture>
  <source srcset="/Upload/2024/03/11/tn-w200-frantisek.webp 1x,
                   /Upload/2024/03/11/tn-w300-frantisek.webp 1.5x,
                   /Upload/2024/03/11/tn-w400-frantisek.webp 2x,
                   /Upload/2024/03/11/tn-w600-frantisek.webp 3x">
  <img src="/Upload/2024/03/11/frantisek.jpg" width="2222" height="2963" loading="lazy" alt="Video poster">
</picture>
Enter fullscreen mode Exit fullscreen mode

Handling Large Images

If we, as developers, avoid perfect resizing and conversion of pictures, we can't expect common admins or clients to do it. Therefore, we let people upload what they want, and we process it (good UX and client relations).

Initially, we didn't expect resizing to be that intensive and we completely killed our demo server couple times. So we developed this approach:

  • We've built a simple API service on a separate low-end VPS dedicated to image processing.
  • Our production servers send the image and desired formats/sizes to this service.
  • The processed images are returned and stored for serving.

The Result

  • Users are happy: Images load quickly and look great on any device.
  • Admins are happy: They don't have to worry about resizing or optimizing images before uploading.
  • Developers are happy: The process is automated, with no additional work required.
  • Servers are happy: Reduced bandwidth consumption and CPU load for serving pictures.

I know we could use any of the public commercial services for picture resizing, but to be honest, this was an afternoon of work (meaning - way cheaper) and we have all aspects under control.

Do We Release It Commercially?

We are so happy with this solution that we're thinking about polishing this resize and conversion service and enabling it for your use. Is there any interest in such a service? Let me know. Maybe we can cut a great deal.

For Developers, By Developers - possibly a meme at this point, but hard truth in this case.

The Code (Simplified)

I can't share the resizing aspects, but I can show you the selection and <picture> creation. We work in PHP, and here's how we do it.

Generating the <picture> Element

function generatePictureElement($imagePath, $width = null, $height = null, $alt = '') {
    $multipliers = [1, 1.5, 2, 2.5, 3];
    $srcset = [];
    foreach ($multipliers as $multiplier) {
        $resizedWidth = $width ? intval($width * $multiplier) : null;
        $resizedHeight = $height ? intval($height * $multiplier) : null;
        $resizedImagePath = getResizedImagePath($imagePath, $resizedWidth, $resizedHeight);
        if ($resizedImagePath) {
            $srcset[] = $resizedImagePath . ' ' . $multiplier . 'x';
        }
    }

    $srcsetAttribute = implode(', ', $srcset);
    $originalImagePath = getOriginalImagePath($imagePath);

    return "
    <picture>
        <source srcset=\"$srcsetAttribute\">
        <img src=\"$originalImagePath\" alt=\"$alt\" loading=\"lazy\">
    </picture>
    ";
}
function getResizedImagePath($imagePath, $width = null, $height = null) {
    $formats = ['avif', 'webp', 'jpg', 'png', 'gif'];

    // Determine the directory and filename
    $pathInfo = pathinfo($imagePath);
    $dir = $pathInfo['dirname'];
    $filename = $pathInfo['filename'];
    $ext = $pathInfo['extension'];

    // Decide which formats to check based on browser support
    if (supportsAVIF()) {
        $preferredFormats = ['avif', 'webp', 'jpg', 'png', 'gif'];
    } elseif (supportsWebP()) {
        $preferredFormats = ['webp', 'jpg', 'png', 'gif'];
    } else {
        $preferredFormats = ['jpg', 'png', 'gif'];
    }

    // Loop through preferred formats to find the smallest suitable image
    foreach ($preferredFormats as $format) {
        $resizedImageName = "{$filename}-w{$width}-h{$height}.{$format}";
        $resizedImagePath = "{$dir}/{$resizedImageName}";

        if (file_exists($resizedImagePath)) {
            // Resized image exists, return its path
            return $resizedImagePath;
        }
    }

    // Resized image doesn't exist, queue it for processing
    queueImageForProcessing($imagePath, $width, $height, $preferredFormats);

    // Return original image path as a fallback
    return $imagePath;
}
function supportsWebP() {
    return isset($_SERVER['HTTP_ACCEPT']) && strpos($_SERVER['HTTP_ACCEPT'], 'image/webp') !== false;
}

function supportsAVIF() {
    return isset($_SERVER['HTTP_ACCEPT']) && strpos($_SERVER['HTTP_ACCEPT'], 'image/avif') !== false;
}
Enter fullscreen mode Exit fullscreen mode

We use a custom templating system where HTML is mixed with PHP (I know it’s not common, but it works best for our needs).

<div class="Image">
    <?php echo generatePictureElement('/path/to/image.png', 500, alt: 'Image alt description'); ?>
</div>
Enter fullscreen mode Exit fullscreen mode

This code shows a logic. What we actually do is we have a database of files and their variants, so instead of paths, we work with arrays and objects. But, as I said, this beautifully shows the logic of what we do.

And as you can see, if front-end dev writes this code or <img> tag, who cares in regards to time spent.

Let's Point Out Issues

This method is built such that it doesn't cause any issues in the real world. But it has two areas of improvement which I can't wait to nail down (yet again, just out of perfectionist’s perspective).

  1. Double Queueing

    It queues for resizing after the first load of that image on the front end. If the website has high traffic, it can actually queue it twice. In real-world use - it's fine; so it processes twice. It happened only twice in a year on high-traffic websites.

  2. Initial Display of Large Images

    If an admin uploads large pictures, it takes 5–10 minutes to resize and convert since it's happening in scheduled tasks. Meanwhile, the large pictures are displayed on their webpages. In real-world use - usually only the admin sees it because new content often takes a little bit before users visit it. On high-traffic websites, we shorten this timer to as low as 1 minute, so again, not an issue.

Storage Considerations

You might point out that now we have multiple images for every size and format, which could be storage-intensive. If your website is full of pictures and galleries, and you need thumbnails and detailed images for each, yes, it will use a lot of storage.

But disk space is the cheapest component these days. The benefits in performance and user experience usually far outweigh the costs. And with that, you've lowered your CPU and entire infrastructure load.

One side note - Why care about infrastructure? Someone else is dealing with that, right? That's what I hear a lot. The simple fact is that it's not true. If a hosting company has to add hardware for higher bandwidth due to increased loads without more customers, they won't be able to sustain it and will happily pass those costs to the server renters.

Our Real-World Results And Experience

In Google Search Console, Core Web Vitals are greener than ever, load times are generally much lower, and visitor session times are higher (a couple of percent, but I'll take it!). And there's no additional work for developers or admins - everything just works smoothly.

I highly recommend every dev use something like this. It did a lot for us while we could actually forget this thing in the background exists and does its job. And I am literally writing this article longer than it was to set up and polish all the systems.

Conclusion

By automating image optimization, we've achieved:

  • Perfect image delivery tailored to each device.
  • Improved page load times and user experience.
  • Reduced manual work for developers.

If you're tired of the chore of image optimization, consider automating the process. It's a win-win for everyone involved.

Do you use some systemic solutions for your images?

Or do you manually convert them?

Have you tried AVIFs? They have amazing benefits for larger images.

Top comments (0)