Skip to content
DERKONLINE

Build an Image Pipeline That Serves the Perfect Byte Count

Automate AVIF/WebP fallback, srcset sizing, lazy loading, and layout reservation so every device gets the smallest sharp image.

Derrick S. K. Siawor7 min read

Most performance problems on a marketing site trace back to one thing: images. A hero photo shipped at 3500 pixels wide to a 390-pixel phone, encoded as a baseline JPEG, downloads megabytes the visitor never needed and pushes Largest Contentful Paint past the point where people start leaving. The fix is not a one-off crunch in Photoshop. It is a pipeline that, for every image and every device, serves the smallest byte count that still looks sharp.

That pipeline has four moving parts, and once they are wired together correctly you stop thinking about images at all. The browser asks for exactly what it can use, your origin or CDN answers in the best format that browser supports, and the layout never jumps. This is how every image on a fast site behaves whether the team remembers to optimize or not.

Pick the format the browser can actually decode

In 2026 you have three formats worth serving. AVIF is the smallest, roughly 50 percent smaller than a comparable JPEG at the same perceived quality. WebP is the dependable middle, around 25 to 35 percent smaller than JPEG and supported everywhere a real user might be. JPEG is the universal floor that nothing fails to decode.

The mistake is choosing one format globally. The right move is format negotiation: offer AVIF first, fall back to WebP, fall back to JPEG, and let the browser take the best one it understands. The <picture> element gives you explicit control over that fallback chain.

<picture>
  <source type="image/avif" srcset="hero-800.avif 800w, hero-1600.avif 1600w" sizes="100vw">
  <source type="image/webp" srcset="hero-800.webp 800w, hero-1600.webp 1600w" sizes="100vw">
  <img src="hero-1600.jpg" alt="" width="1600" height="900" loading="eager" fetchpriority="high">
</picture>

The browser reads the <source> elements top to bottom and uses the first type it can decode. A Chrome or Safari visitor gets AVIF. An older client falls through to the JPEG <img>. You never have to detect the browser yourself, and you never ship a format that 404s on someone's screen.

Serve the right pixel count with srcset and sizes

Format only solves half the problem. A 1600-pixel AVIF is still wasteful on a phone that can only show 390 CSS pixels. This is what srcset and sizes exist for, and it is the single biggest improvement most teams skip.

Generate at least three width variants for content images: 400w, 800w, and 1200w. For full-bleed heroes add 1600w and 2000w so retina laptops and large monitors stay crisp. The srcset attribute lists each variant with its intrinsic width, and sizes tells the browser how wide the image will render at the current breakpoint so it can pick the smallest file that covers that box.

<img
  srcset="card-400.avif 400w, card-800.avif 800w, card-1200.avif 1200w"
  sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 400px"
  src="card-800.avif" alt="..." width="400" height="300" loading="lazy">

Get sizes wrong and the whole mechanism backfires. If you declare 100vw for an image that actually renders at 400px, the browser downloads a full-viewport-width file for a thumbnail. Match sizes to the real CSS layout, breakpoint by breakpoint, and the device pulls exactly what fits.

Automate generation instead of hand-cutting variants

No one is going to export 15 files per image by hand, and they should not have to. The variants come out of a build step or a CDN, never a designer's manual export.

The build-step approach keeps source images in their original JPEG or PNG and uses a library like sharp to emit every AVIF, WebP, and JPEG variant at every width during the build. This is deterministic, version-controlled, and free at runtime. The CDN approach keeps one high-quality master and lets an image CDN convert and resize on the fly based on a query string and the Accept header, then caches the result at the edge worldwide. Both are valid. We tend to reach for the build step on content-stable marketing sites and the CDN when an app accepts user uploads at unknown sizes.

Either way the rule from our engineering standards holds: the dev or build script bootstraps the whole thing, the same way one command should bring up your whole stack. Nobody on the team should have to remember to run an optimizer before they push. The pipeline that ships fast images is the same pipeline that builds the site. This kind of automation, where the right thing happens by default instead of by discipline, is exactly what we bake into every web app we build.

Lazy-load below the fold, prioritize above it

Loading strategy is the cheapest win on the list. The hero image, the LCP element, gets loading="eager" and fetchpriority="high" so the browser fetches it before anything else competes for bandwidth. Everything below the fold gets loading="lazy" so it never downloads until the visitor scrolls near it.

This matters most on long pages. A pricing page with 40 logo images or a portfolio with dozens of project thumbnails should not fire 40 requests on first paint. Native loading="lazy" defers each one until it approaches the viewport. The same principle scales to lists and grids: at 10 items the page is fine, at 500 it melts unless off-screen assets stay unloaded, the kind of long task on the main thread that drags interactivity down, and the same restraint you apply to the third-party scripts wrecking your page speed. We treat that as a baseline, not a future enhancement, the same way we treat any list or grid that could grow.

Reserve the space so the layout never jumps

Every <img> needs explicit width and height attributes, even when CSS controls the displayed size. Those attributes let the browser compute the aspect ratio and reserve the exact box before the bytes arrive. Without them the page reflows when each image loads, content jumps under the reader's thumb, and your Cumulative Layout Shift score tanks. With them the skeleton holds its shape and the image drops into place.

Pair this with a low-quality placeholder where it helps: a tiny blurred version or a solid average-color box that occupies the reserved space until the real image decodes. The visitor sees structure immediately instead of a blank rectangle, the same optimistic, skeleton-driven feel that makes a slow load read as fast, and the final image arrives without a single pixel shifting.

Putting the pipeline together

The whole system reads as one decision tree the browser walks on every image:

Decision tree the browser walks per image for format pixel count loading priority and reserved box

  • What format can I decode? Take AVIF, else WebP, else JPEG, resolved by <picture> and type.
  • How many pixels do I need for this box? Pick the smallest srcset width that covers the sizes declaration.
  • Do I need this now? Eager-load and prioritize above the fold, lazy-load below it.
  • Where does it go? The reserved width/height box, so nothing shifts.

Wire those four together and the result is not a faster image here and there. It is a site where every image, on every device, arrives at the smallest byte count that still looks right, automatically, for as long as the project lives. That is the difference between a one-time optimization that decays the moment someone uploads a new photo and a pipeline that holds the line on its own. And because images are rarely the only thing slowing a page, the same byte-discipline belongs upstream too, in slashing time to first byte with streaming server rendering so the document itself arrives fast.

If your Largest Contentful Paint is being dragged down by oversized media and you would rather have this built once and built right, that is the kind of performance work we do on every site we ship. The wins only count if they hold up for real visitors, which is why we trust field data over lab scores to confirm a lighter page actually loads faster where it matters, since every slow second costs real revenue.