DEV Community

Marcc Atayde
Marcc Atayde

Posted on

Image Optimization Beyond Compression: Lazy Loading, Modern Formats, and the Rendering Pipeline Tricks That Actually Help

Most developers stop at squishing a JPEG and calling it done. Run it through ImageOptim, drop it into the public/ folder, ship it. Job done. Except it's not — because image optimization in 2025 is a multi-layered problem that spans format selection, delivery strategy, browser rendering behavior, and server-side generation pipelines. Get any one of those layers wrong and you're leaving real performance on the table, even if your file sizes look great.

This article goes past the basics. We're talking about the rendering pipeline decisions that affect Largest Contentful Paint (LCP), the loading and fetchpriority attributes that most developers use incorrectly, how to wire up responsive images properly in a Laravel/Blade context, and why AVIF isn't always better than WebP despite what the benchmarks suggest.

Understanding the Rendering Pipeline First

Before you touch a format or a build tool, understand what the browser actually does with an image. The browser parses HTML, discovers image resources, queues them in the network priority system, downloads them, decodes them in the main thread (or off-thread for some formats), and then composites them onto the page.

The decode step is where a lot of developers get surprised. A 200 KB WebP that decodes to a 4000×3000 bitmap is pushing 45 MB into memory. The browser has to handle that. For images that appear in the viewport immediately, you want decode to be fast. For images below the fold, you want it deferred entirely.

This is why the decoding attribute matters:

<!-- Hero image — decode synchronously, don't block compositing -->
<img
  src="/images/hero.webp"
  decoding="sync"
  fetchpriority="high"
  alt="Hero banner"
/>

<!-- Below-fold image — decode asynchronously, don't block rendering -->
<img
  src="/images/product-shot.webp"
  decoding="async"
  loading="lazy"
  alt="Product detail"
/>
Enter fullscreen mode Exit fullscreen mode

Most tutorials tell you to slap loading="lazy" on everything. Don't. Any image in the initial viewport — especially your LCP candidate — should never be lazy loaded. You're telling the browser to deprioritize the most important visual on the page.

Format Selection: AVIF, WebP, and the Reality of Decode Cost

AVIF offers better compression than WebP at equivalent quality. That's real. But AVIF has a higher CPU decode cost, which matters on low-end mobile devices. If your audience skews toward budget Android phones — which is increasingly common in markets across the Middle East, South Asia, and Southeast Asia — AVIF can actually hurt perceived performance even when the file is smaller, because the decode blocks rendering longer.

The pragmatic approach: use AVIF for static, below-fold images and large hero backgrounds where the browser has time to decode off the critical path. Stick with WebP for product images, thumbnails, and anything in the viewport on page load.

In a Laravel application, you can automate this with Spatie's Laravel Image package alongside a queue-driven conversion pipeline:

// app/Jobs/ProcessProductImage.php

use Spatie\Image\Image;
use Spatie\Image\Enums\ImageDriver;

class ProcessProductImage implements ShouldQueue
{
    public function __construct(
        private string $sourcePath,
        private string $publicDir
    ) {}

    public function handle(): void
    {
        $baseName = pathinfo($this->sourcePath, PATHINFO_FILENAME);

        // Generate WebP for viewport-critical delivery
        Image::useImageDriver(ImageDriver::Gd)
            ->loadFile($this->sourcePath)
            ->quality(82)
            ->save("{$this->publicDir}/{$baseName}.webp");

        // Generate AVIF for below-fold / preload link usage
        Image::useImageDriver(ImageDriver::Imagick)
            ->loadFile($this->sourcePath)
            ->quality(70)
            ->save("{$this->publicDir}/{$baseName}.avif");

        // Responsive sizes
        foreach ([400, 800, 1200] as $width) {
            Image::useImageDriver(ImageDriver::Gd)
                ->loadFile($this->sourcePath)
                ->width($width)
                ->quality(80)
                ->save("{$this->publicDir}/{$baseName}-{$width}.webp");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Dispatch this on the saved observer of your model and you've got a fully automated responsive image pipeline that runs off the request cycle.

Responsive Images: srcset and sizes Done Correctly

The srcset attribute is widely used and widely misunderstood. The w descriptor tells the browser the intrinsic width of the image source. The sizes attribute tells the browser what rendered size the image will occupy at various viewport widths. The browser combines these two pieces of information with the device pixel ratio to pick the optimal source.

If your sizes attribute is wrong, the browser makes bad choices. Here's a real-world example for a three-column product grid that goes full-width on mobile:

<picture>
  <source
    type="image/avif"
    srcset="
      /images/product-400.avif  400w,
      /images/product-800.avif  800w,
      /images/product-1200.avif 1200w
    "
    sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
  />
  <source
    type="image/webp"
    srcset="
      /images/product-400.webp  400w,
      /images/product-800.webp  800w,
      /images/product-1200.webp 1200w
    "
    sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
  />
  <img
    src="/images/product-800.webp"
    alt="Product name"
    loading="lazy"
    decoding="async"
    width="800"
    height="600"
  />
</picture>
Enter fullscreen mode Exit fullscreen mode

Always include explicit width and height attributes on the <img> element. This is how the browser reserves layout space before the image loads, eliminating Cumulative Layout Shift (CLS) — a Core Web Vitals metric that tanks rankings and feels broken to users.

Building a Blade Component for Consistency

Repeating the <picture> pattern by hand across a Laravel application is error-prone. Wrap it in a Blade component:

// app/View/Components/ResponsiveImage.php

class ResponsiveImage extends Component
{
    public function __construct(
        public string $src,
        public string $alt,
        public int $width,
        public int $height,
        public string $sizes = '100vw',
        public bool $priority = false,
    ) {}

    public function render()
    {
        return view('components.responsive-image');
    }

    public function baseName(): string
    {
        return pathinfo($this->src, PATHINFO_FILENAME);
    }

    public function dir(): string
    {
        return pathinfo($this->src, PATHINFO_DIRNAME);
    }
}
Enter fullscreen mode Exit fullscreen mode
{{-- resources/views/components/responsive-image.blade.php --}}
<picture>
  <source
    type="image/avif"
    srcset="
      {{ $dir() }}/{{ $baseName() }}-400.avif 400w,
      {{ $dir() }}/{{ $baseName() }}-800.avif 800w,
      {{ $dir() }}/{{ $baseName() }}-1200.avif 1200w
    "
    sizes="{{ $sizes }}"
  />
  <source
    type="image/webp"
    srcset="
      {{ $dir() }}/{{ $baseName() }}-400.webp 400w,
      {{ $dir() }}/{{ $baseName() }}-800.webp 800w,
      {{ $dir() }}/{{ $baseName() }}-1200.webp 1200w
    "
    sizes="{{ $sizes }}"
  />
  <img
    src="{{ $dir() }}/{{ $baseName() }}-800.webp"
    alt="{{ $alt }}"
    width="{{ $width }}"
    height="{{ $height }}"
    @if($priority)
      fetchpriority="high"
      decoding="sync"
    @else
      loading="lazy"
      decoding="async"
    @endif
  />
</picture>
Enter fullscreen mode Exit fullscreen mode

Usage becomes clean and intention-revealing:

{{-- Hero image — prioritized --}}
<x-responsive-image
  src="/images/hero"
  alt="Company hero banner"
  :width="1200"
  :height="630"
  sizes="100vw"
  :priority="true"
/>

{{-- Product card — lazy --}}
<x-responsive-image
  src="/images/product-{{ $product->id }}"
  alt="{{ $product->name }}"
  :width="800"
  :height="600"
  sizes="(max-width: 640px) 100vw, 33vw"
/>
Enter fullscreen mode Exit fullscreen mode

CDN Delivery and Cache-Control Headers

None of this matters if you're serving images from the same server handling PHP requests. Static assets should be on a CDN. Laravel's config/filesystems.php makes it straightforward to push to S3-compatible storage with CloudFront or Cloudflare in front:

// config/filesystems.php
'images' => [
    'driver' => 's3',
    'key' => env('AWS_ACCESS_KEY_ID'),
    'secret' => env('AWS_SECRET_ACCESS_KEY'),
    'region' => env('AWS_DEFAULT_REGION'),
    'bucket' => env('AWS_BUCKET'),
    'url' => env('CDN_URL'),
    'visibility' => 'public',
    'options' => [
        'CacheControl' => 'public, max-age=31536000, immutable',
    ],
],
Enter fullscreen mode Exit fullscreen mode

The immutable directive tells browsers and CDN edge nodes that this URL's content will never change. Since your processed images are content-addressed (or version-stamped through your deployment pipeline), this is safe — and it eliminates revalidation requests entirely.

This is the kind of compounding infrastructure work that teams at HanzWeb Agency build into projects from the start rather than retrofitting later, because it's significantly harder to restructure a delivery pipeline after an application is in production.

Conclusion

Image optimization isn't a one-time task — it's a pipeline decision made at architecture time. The format you choose, the attributes you set, the responsive breakpoints you define, and the CDN configuration you deploy all interact with each other and with the browser's rendering engine in ways that file size alone can't capture.

The takeaway: audit your LCP image first. Make sure it has fetchpriority="high" and no loading="lazy". Add explicit dimensions to every image. Build a format conversion job that generates WebP and AVIF variants at multiple widths. Then set aggressive cache headers and put a CDN in front of everything. That sequence, applied consistently, will move your Core Web Vitals scores in ways that running files through a compressor never will.

Top comments (0)