Images are often the largest payload on a web page. A site can have clean HTML, optimized CSS, and fast JavaScript, yet still load slowly because a 2,500px image is being downloaded to fill a 400px mobile container.
The solution is not simply "compress every image."
Modern image optimization combines the right format, the right dimensions, the right compression level, and the right loading strategy. WebP and AVIF can reduce file size compared with JPEG and PNG, while srcset and sizes prevent browsers from downloading unnecessarily large images. Browser-level lazy loading can then defer images that are not needed for the initial viewport.
The objective is to make every image appropriate for the device and display size without destroying visual quality.
For many image-heavy pages, this can produce dramatic payload reductions. A 1 MB JPEG does not necessarily need to become a 300 KB JPEG. It may become a 150 KB AVIF or WebP and be served at the correct dimensions for the user's screen.
This guide shows exactly how to implement that workflow using native HTML and CSS.
JPEG and PNG vs. WebP and AVIF
The first optimization decision is the image format. JPEG remains useful for photographs and situations where maximum legacy compatibility is important. PNG is particularly useful when lossless quality or transparency is required. But WebP and AVIF generally provide better compression for many modern web use cases.
| Format | Compression | Transparency | Best Use | Browser Compatibility |
|---|---|---|---|---|
| JPEG | Lossy | No | Photographs | Excellent |
| PNG | Lossless / limited lossy workflows | Yes | UI graphics, transparency, screenshots | Excellent |
| WebP | Lossy + lossless | Yes | General web images | Excellent in modern browsers |
| AVIF | Lossy + lossless | Yes | Maximum compression efficiency | Broad modern-browser support |
The important point is that you do not need to choose one format for every visitor. HTML allows you to offer AVIF first, WebP second, and a JPEG fallback.
Use <picture> for Format Negotiation
A production-friendly implementation looks like this:
<picture>
<source
srcset="/images/hero.avif"
type="image/avif"
>
<source
srcset="/images/hero.webp"
type="image/webp"
>
<img
src="/images/hero.jpg"
width="1600"
height="900"
alt="Business team working together"
>
</picture>The browser evaluates the <source> elements and selects a compatible image. If AVIF is supported, it uses the AVIF version. If not, it tries WebP and eventually falls back to JPEG. The <img> remains essential because it provides the actual image element and fallback. This is much safer than attempting to detect browser support with JavaScript.
Add Responsive Variants with srcset
Format selection solves one problem, but it does not solve image dimensions. Suppose your desktop hero is 1600 × 900. Sending that same file to a phone displaying the image at approximately 390px wide is wasteful.
Create several versions (hero-480.avif, hero-768.avif, hero-1200.avif, hero-1600.avif) and use srcset:
<picture>
<source
type="image/avif"
srcset="
/images/hero-480.avif 480w,
/images/hero-768.avif 768w,
/images/hero-1200.avif 1200w,
/images/hero-1600.avif 1600w
"
>
<source
type="image/webp"
srcset="
/images/hero-480.webp 480w,
/images/hero-768.webp 768w,
/images/hero-1200.webp 1200w,
/images/hero-1600.webp 1600w
"
>
<img
src="/images/hero-1200.jpg"
srcset="
/images/hero-480.jpg 480w,
/images/hero-768.jpg 768w,
/images/hero-1200.jpg 1200w,
/images/hero-1600.jpg 1600w
"
sizes="100vw"
width="1600"
height="900"
alt="Business team working together"
>
</picture>The w descriptors describe the intrinsic width of each candidate. The browser combines these candidates with sizes and the device's pixel density to select an appropriate resource.
Configure the sizes Attribute Accurately
Consider a product card that occupies half the desktop content area (50vw) but the full width on mobile (100vw):
<img
src="/images/product-1200.jpg"
srcset="
/images/product-400.jpg 400w,
/images/product-800.jpg 800w,
/images/product-1200.jpg 1200w
"
sizes="
(max-width: 767px) 100vw,
50vw
"
width="1200"
height="800"
alt="Product"
>The browser can then choose an appropriate candidate instead of blindly downloading the largest file.
Lists each file path alongside its true pixel width (400w, 800w, 1200w).
Tells the browser the layout width at each breakpoint before external CSS finishes loading.
Multiplies rendered slot width by device pixel ratio (1x, 2x) and requests the optimal file.
Handle Retina Displays Without Sending Oversized Files to Everyone
A 400 CSS-pixel-wide image may need an image resource substantially wider than 400 physical pixels on a high-DPI screen. For fixed-width elements like logos, you can provide density-specific resources:
<img
src="/images/logo.png"
srcset="
/images/logo.png 1x,
/images/logo@2x.png 2x
"
width="300"
height="100"
alt="Company logo"
>For fluid layouts, however, width descriptors such as 400w, 800w, and 1200w combined with sizes are usually more flexible.
The Golden Rule of Lazy Loading
Lazy loading is one of the easiest image optimizations to implement incorrectly. For images below the initial viewport:
<img
src="/images/product-01.webp"
width="800"
height="600"
loading="lazy"
alt="Product photograph"
>The browser can defer loading the image until it is approaching the viewport. But there is one critical exception.
Never Lazy Load the LCP Hero Image
Do not put loading="lazy" on your hero image if that image is the page's Largest Contentful Paint element. Lazy loading can delay its discovery and download. Instead:
<img
src="/images/hero.webp"
width="1600"
height="900"
fetchpriority="high"
alt="Hero image"
>Google's web.dev guidance explicitly warns against lazy-loading the LCP image and recommends fetchpriority="high" when an image is likely to be the LCP element. Use it selectively—putting high priority on too many resources weakens its effect.
- DO NOT add
loading="lazy" - Add
fetchpriority="high" - Include explicit
width&height
- Add native
loading="lazy" - Use
srcset&sizesfor responsive cards - Include explicit
width&heightto prevent CLS
Compressing Images Without Destroying Sharpness
Compression is not about making every image as small as technically possible. It is about finding the smallest file that still looks correct at its actual display size.
Photographs tolerate lossy compression relatively well. But screenshots and UI graphics are different—sharp text, thin lines, icons, and high-contrast edges expose compression artifacts much more easily.
Don't Compress a UI Screenshot Like a Photograph
Aggressive lossy compression on UI screenshots creates fuzzy text, ringing around letters, blocky edges, and blurry icons. Test WebP or AVIF carefully and compare the actual rendered result, or use lossless WebP/PNG where sharp text and transparency are critical.
Compress at the Intended Display Size (Don't Resize Only With CSS)
Instead of aggressively compressing a 4000 × 2500 file that displays at 800 × 500, resize it to approximately the largest size you actually need first. This reduces both file bytes and image decoding/rendering work.
Remember: .hero img { width: 100%; } controls display size, not downloaded file size. Combine CSS with srcset and sizes so the browser downloads the smaller resource.
A Practical 8-Step Image Compression Workflow
For a custom website, use this workflow for every major image:
1. Identify the purpose
Determine whether the asset is a photograph, logo, screenshot, UI graphic, icon, decorative background, or hero/LCP image.
2. Resize the source
Do not upload a 5000px image when the largest rendered size is 1200px.
3. Generate modern formats
Create image.avif, image.webp, and image.jpg where they provide a meaningful size/quality advantage.
4. Generate responsive widths
Export breakpoints such as 400px, 800px, 1200px, and 1600px based on the component's actual rendered sizes.
5. Add intrinsic dimensions
Include width="1200" height="800" on the <img> tag so the browser reserves layout space and prevents CLS.
6. Set loading behavior
Use loading="lazy" below the fold, and fetchpriority="high" (without lazy loading) on the LCP hero image.
7. Test visual quality
Open the actual image at its rendered size and inspect text, faces, edges, gradients, shadows, fine details, and transparency.
8. Test the page in DevTools and Field Data
Use the Chrome DevTools Network panel, Lighthouse/PageSpeed Insights, and Performance panel to verify which image candidate was actually downloaded and whether the LCP image starts early enough.
Common Image Optimization Mistakes
Audit your templates against the 6 classic mistakes
- Mistake 1: Lazy-loading everything: Putting
loading="lazy"on every image delays the LCP element. - Mistake 2: Using a 3000px image for a 300px card: CSS resizing doesn't reduce network payload. Use
srcsetandsizes. - Mistake 3: Converting everything blindly to AVIF: Measure actual output size and visual quality rather than assuming AVIF is always smaller.
- Mistake 4: Removing width and height: Responsive images still need HTML dimensions to establish intrinsic aspect ratio.
- Mistake 5: Compressing screenshots like photographs: Aggressive lossy settings ruin UI text and sharp lines.
- Mistake 6: Using JavaScript for basic lazy loading: Native
loading="lazy"handles normal offscreen images without extra JS overhead.
The Production Checklist
Verify all 15 production image standards before launch
Before launching a custom website, audit every significant image against this list:
srcset configured for responsive imagessizes accurately reflects rendered widthwidth and height attributes includedloading="lazy"fetchpriority="high"Conclusion
Combine format, dimensions, responsiveness, and priority
Modern image optimization is not simply "convert JPG to WebP." The real performance improvement comes from combining several decisions:
Use AVIF and WebP where they make sense, keep reliable fallbacks, and let the browser choose between appropriately sized candidates with <picture>, srcset, and sizes.
Most importantly, do not lazy-load your LCP hero image. Critical images need to be discoverable and prioritized early, while images below the fold can be deferred with native loading="lazy".
The objective isn't to make images tiny. It's to deliver the smallest image that looks correct, at exactly the time and resolution the user needs it.
How much image payload could your website eliminate if every image were delivered in the correct format, dimensions, and loading priority?