Lighthouse Image Audit: Fix Every Image Warning
Fix all Lighthouse image-related warnings and opportunities. Step-by-step solutions for properly size images, serve modern formats, defer offscreen images, and more.
Lighthouse flags image issues that directly hurt your performance score and user experience. This guide covers every image-related audit, explains exactly what triggers each one, and provides copy-paste solutions to fix them.
Understanding Lighthouse Image Audits
Lighthouse groups image findings into two categories:
| Category | Meaning | Score Impact |
|---|---|---|
| Opportunities | Changes that could improve load time | Directly reduces Performance score |
| Diagnostics | Information about best practices | May affect score indirectly |
Where Image Audits Appear
Image-related findings show up across multiple sections of a Lighthouse report:
- Performance > Opportunities: Properly size images, serve next-gen formats, defer offscreen images, efficiently encode images
- Performance > Diagnostics: Avoid enormous network payloads, image elements do not have explicit width and height
- Best Practices: Image display dimensions, image aspect ratio
- SEO: Image elements do not have alt attributes (covered in our alt text guide)
How Scoring Works
Lighthouse calculates potential savings in bytes for each opportunity. The more bytes you could save, the more the audit impacts your score. An image opportunity saving 2 MB matters far more than one saving 20 KB.
The Performance score weighs metrics, not audits directly. But fixing image audits improves the underlying metrics:
| Image Fix | Metric Improved | Typical Impact |
|---|---|---|
| Compress images | LCP, Speed Index | 0.5-3s faster |
| Modern formats | LCP, Speed Index | 0.3-2s faster |
| Lazy load offscreen | Speed Index, TTI | 0.5-4s faster |
| Preload LCP image | LCP | 0.2-1s faster |
| Set dimensions | CLS | 0.05-0.3 CLS reduction |
”Properly Size Images”
What Triggers This Audit
Lighthouse flags images where the rendered size is significantly smaller than the downloaded size. If you download a 2000px-wide image but display it at 400px, Lighthouse calculates the wasted bytes.
The audit triggers when an image could save at least 4 KiB by being properly sized.
The exact Lighthouse message:
Properly size images — Serve images that are appropriately-sized to save cellular data and improve load time.
Why It Matters
Oversized images are the single most common performance problem. A 4000x3000 photo displayed at 800x600 wastes 93% of its pixels.
How to Calculate Correct Sizes
Step 1: Determine the rendered size in CSS pixels:
// In DevTools Console
document.querySelectorAll('img').forEach(img => {
const rendered = `${img.clientWidth}x${img.clientHeight}`;
const natural = `${img.naturalWidth}x${img.naturalHeight}`;
const ratio = ((img.naturalWidth * img.naturalHeight) /
(img.clientWidth * img.clientHeight)).toFixed(1);
console.log(`${img.src}\n Rendered: ${rendered}, Natural: ${natural}, Oversized: ${ratio}x`);
});
Step 2: Account for device pixel ratio (DPR). Retina displays need 2x pixels:
| Display | DPR | CSS Width | Image Width Needed |
|---|---|---|---|
| Standard | 1x | 800px | 800px |
| Retina | 2x | 800px | 1600px |
| High-end mobile | 3x | 400px | 1200px |
Rule of thumb: Serve images at 2x the rendered CSS width. Going to 3x adds file size with minimal visual benefit.
The Fix: Responsive Images
<!-- BEFORE: One massive image for everyone -->
<img src="hero-4000.jpg" alt="Hero image">
<!-- AFTER: Responsive images with srcset -->
<img
src="hero-800.jpg"
srcset="
hero-400.jpg 400w,
hero-800.jpg 800w,
hero-1200.jpg 1200w,
hero-1600.jpg 1600w
"
sizes="(max-width: 600px) 100vw, (max-width: 1200px) 50vw, 800px"
alt="Hero image"
width="800"
height="450"
>
Generating Responsive Variants
With Sharp (Node.js):
const sharp = require('sharp');
const widths = [400, 800, 1200, 1600];
async function generateResponsive(inputPath, outputDir) {
for (const width of widths) {
await sharp(inputPath)
.resize(width, null, { withoutEnlargement: true })
.jpeg({ quality: 80, mozjpeg: true })
.toFile(`${outputDir}/hero-${width}.jpg`);
// Also generate WebP variants
await sharp(inputPath)
.resize(width, null, { withoutEnlargement: true })
.webp({ quality: 80 })
.toFile(`${outputDir}/hero-${width}.webp`);
}
}
generateResponsive('./hero-original.jpg', './dist/images');
With a CDN like Sirv, you skip generating variants entirely:
<img
src="https://yoursite.sirv.com/hero.jpg?w=800"
srcset="
https://yoursite.sirv.com/hero.jpg?w=400 400w,
https://yoursite.sirv.com/hero.jpg?w=800 800w,
https://yoursite.sirv.com/hero.jpg?w=1200 1200w,
https://yoursite.sirv.com/hero.jpg?w=1600 1600w
"
sizes="(max-width: 600px) 100vw, (max-width: 1200px) 50vw, 800px"
alt="Hero image"
width="800"
height="450"
>
Sirv dynamically resizes on request and caches the result at the edge. No build step, no stored variants.
Savings Calculation
| Scenario | Original | Properly Sized | Savings |
|---|---|---|---|
| 4000px image at 800px render | 2.4 MB | 180 KB | 92% |
| 2000px hero at 1200px render | 800 KB | 320 KB | 60% |
| 3000px thumbnail at 200px render | 1.8 MB | 15 KB | 99% |
“Serve Images in Next-Gen Formats”
What Triggers This Audit
Lighthouse flags images served as JPEG, PNG, or GIF when WebP or AVIF would be significantly smaller.
The exact Lighthouse message:
Serve images in next-gen formats — Image formats like WebP and AVIF often provide better compression than PNG or JPEG, which means faster downloads and less data consumption.
Why It Matters
Modern formats deliver the same visual quality at substantially smaller file sizes:
| Format | Typical Savings vs JPEG | Browser Support |
|---|---|---|
| WebP | 25-35% smaller | 97% globally |
| AVIF | 40-50% smaller | 92% globally |
The Fix: <picture> Element
The <picture> element lets browsers choose the best supported format:
<picture>
<!-- AVIF: best compression, good support -->
<source srcset="hero.avif" type="image/avif">
<!-- WebP: great compression, widest support -->
<source srcset="hero.webp" type="image/webp">
<!-- JPEG: fallback for all browsers -->
<img src="hero.jpg" alt="Hero image" width="1200" height="600">
</picture>
With Responsive Images
Combine format negotiation with responsive sizing:
<picture>
<source
type="image/avif"
srcset="hero-400.avif 400w, hero-800.avif 800w, hero-1200.avif 1200w"
sizes="(max-width: 600px) 100vw, (max-width: 1200px) 50vw, 800px"
>
<source
type="image/webp"
srcset="hero-400.webp 400w, hero-800.webp 800w, hero-1200.webp 1200w"
sizes="(max-width: 600px) 100vw, (max-width: 1200px) 50vw, 800px"
>
<img
src="hero-800.jpg"
srcset="hero-400.jpg 400w, hero-800.jpg 800w, hero-1200.jpg 1200w"
sizes="(max-width: 600px) 100vw, (max-width: 1200px) 50vw, 800px"
alt="Hero image"
width="1200"
height="600"
loading="lazy"
>
</picture>
Accept Header Negotiation (Server-Side)
Instead of multiple <source> elements, let the server decide based on the browser’s Accept header:
# Nginx configuration
map $http_accept $webp_suffix {
default "";
"~*image/avif" ".avif";
"~*image/webp" ".webp";
}
location ~* \.(jpg|jpeg|png)$ {
# Try AVIF first, then WebP, then original
try_files $uri$webp_suffix $uri =404;
add_header Vary Accept;
}
Similar rules can be configured for Apache using mod_rewrite to check the Accept header and serve .avif or .webp variants when they exist on disk.
CDN Auto-Format (Easiest Solution)
With Sirv, Cloudinary, or similar CDNs, format negotiation happens automatically:
<!-- Sirv auto-detects browser support and serves the optimal format -->
<img src="https://yoursite.sirv.com/hero.jpg?w=1200" alt="Hero image">
When Chrome requests this URL, Sirv returns AVIF. When Safari requests it, Sirv returns WebP. No <picture> element needed. The Vary: Accept header ensures caches store separate versions per format.
”Efficiently Encode Images”
What Triggers This Audit
Lighthouse re-compresses each image with a quality of 85 and checks if the result is significantly smaller than the served version. If it is, the image is flagged.
The exact Lighthouse message:
Efficiently encode images — Optimized images load faster and consume less cellular data.
Why It Matters
Many images are saved at quality 100 or with unoptimized encoding settings. Simply re-encoding with modern settings can reduce file size 30-60% with no visible quality loss.
Recommended Compression Settings
| Format | Tool | Quality Setting | Expected Savings |
|---|---|---|---|
| JPEG | MozJPEG | 80-85 | 30-50% vs q100 |
| JPEG | libjpeg-turbo | 80-85 | 20-40% vs q100 |
| WebP | libwebp | 75-82 | 25-35% vs JPEG |
| AVIF | libavif | 50-65 | 40-50% vs JPEG |
| PNG | pngquant + oxipng | Quality 80 + strip | 60-80% vs unoptimized |
Quick Fix with Sharp
const sharp = require('sharp');
const { globSync } = require('glob');
// Batch re-encode all JPEGs with MozJPEG
for (const img of globSync('src/images/**/*.{jpg,jpeg}')) {
sharp(img)
.jpeg({ quality: 82, mozjpeg: true, progressive: true })
.toFile(img.replace('src/images', 'dist/images'));
}
Common Causes of Inefficient Encoding
- Exported from Photoshop at quality 100: Default exports are often unoptimized
- Screenshots saved as PNG: Use JPEG/WebP for screenshot photos, PNG only for sharp text
- Stock photos used as-is: Stock sites optimize for quality, not web performance
- CMS uploads without optimization: No compression applied on upload
- Missing metadata stripping: EXIF data can add 50-500 KB
”Defer Offscreen Images”
What Triggers This Audit
Lighthouse flags images that are loaded immediately but are positioned below the fold (outside the initial viewport).
The exact Lighthouse message:
Defer offscreen images — Consider lazy-loading offscreen and hidden images after all critical resources have finished loading to lower time to interactive.
Why It Matters
Loading all images upfront delays the page. On a blog post with 15 images, 80% of them are below the fold. Loading them all immediately wastes bandwidth and competes with critical resources.
The Fix: Native Lazy Loading
<!-- Above the fold: load immediately -->
<img src="hero.jpg" alt="Hero" width="1200" height="600">
<!-- Below the fold: lazy load -->
<img src="blog-photo-1.jpg" alt="..." width="800" height="450" loading="lazy">
<img src="blog-photo-2.jpg" alt="..." width="800" height="450" loading="lazy">
<img src="blog-photo-3.jpg" alt="..." width="800" height="450" loading="lazy">
Critical Rule: Never Lazy Load Above-the-Fold Images
Lazy loading the LCP image delays its appearance and hurts your LCP score:
<!-- WRONG: lazy loading the hero image -->
<img src="hero.jpg" loading="lazy" alt="Hero">
<!-- CORRECT: hero loads immediately, below-fold is lazy -->
<img src="hero.jpg" alt="Hero" fetchpriority="high" width="1200" height="600">
Intersection Observer (Custom Control)
For more control over loading thresholds, use the Intersection Observer API instead of native lazy loading:
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
if (img.dataset.srcset) img.srcset = img.dataset.srcset;
observer.unobserve(img);
}
});
}, { rootMargin: '200px 0px' }); // Start loading 200px before visible
document.querySelectorAll('img[data-src]').forEach(img => observer.observe(img));
How Many Images to Eagerly Load
A practical guideline based on page layout:
| Page Type | Eager Load | Lazy Load |
|---|---|---|
| Landing page (hero only) | 1 image | Everything else |
| Blog post | First 1-2 images | Remaining images |
| Product page | Main product image | Gallery, related items |
| Image gallery | First 4-8 thumbnails | Remaining thumbnails |
| Infinite scroll | First screen | Everything below |
”Image Elements Do Not Have Explicit Width and Height”
What Triggers This Audit
Lighthouse flags <img> elements that are missing width and height attributes.
The exact Lighthouse message:
Image elements do not have explicit width and height — Set an explicit width and height on image elements to reduce layout shifts and improve CLS.
Why It Matters
Without dimensions, the browser does not know how much space to reserve for an image. When the image finally loads, everything below it shifts down. This causes Cumulative Layout Shift (CLS), one of the three Core Web Vitals.
The Fix: Always Set Width and Height
<!-- WRONG: No dimensions -->
<img src="photo.jpg" alt="Photo">
<!-- CORRECT: Dimensions set -->
<img src="photo.jpg" alt="Photo" width="800" height="450">
The browser uses these attributes to calculate the aspect ratio and reserve space before the image loads.
Making It Responsive
Setting width and height does not mean the image renders at fixed dimensions. Use CSS to make it responsive:
img {
max-width: 100%;
height: auto;
}
With this CSS, the browser:
- Reads
width="800"andheight="450"to calculate aspect ratio (16:9) - Reserves space at that aspect ratio within the available width
- Renders the image responsively without layout shift
Using CSS aspect-ratio
For cases where you know the aspect ratio but not exact pixel dimensions:
.hero-image {
aspect-ratio: 16 / 9;
width: 100%;
height: auto;
object-fit: cover;
}
.thumbnail {
aspect-ratio: 1 / 1;
width: 200px;
height: auto;
object-fit: cover;
}
.portrait {
aspect-ratio: 3 / 4;
width: 100%;
height: auto;
object-fit: cover;
}
Common Aspect Ratios
| Ratio | Use Case | Width | Height |
|---|---|---|---|
| 16:9 | Hero images, video thumbnails | 1600 | 900 |
| 4:3 | Product photos, blog images | 1200 | 900 |
| 1:1 | Avatars, social thumbnails | 800 | 800 |
| 3:2 | Photography, DSLR default | 1200 | 800 |
| 21:9 | Panoramic banners | 2100 | 900 |
Framework-Specific Solutions
Most modern frameworks enforce dimensions automatically. Next.js requires width and height as props on its <Image> component (or uses fill mode with a sized parent). Astro infers dimensions from imported images. React frameworks generally make it harder to forget dimensions than plain HTML.
”Preload Largest Contentful Paint Image”
What Triggers This Audit
Lighthouse flags when the LCP element is an image that is not preloaded or given high fetch priority, and the LCP time is slow.
The exact Lighthouse message:
Preload Largest Contentful Paint image — Preload the image used by the LCP element in order to improve your LCP time.
Why It Matters
The browser discovers images in a specific order:
- Parse HTML (finds
<img>tags) - Parse CSS (finds
background-image) - Execute JavaScript (finds dynamically inserted images)
If the LCP image is a CSS background or loaded by JavaScript, the browser discovers it late. Preloading tells the browser to fetch it immediately.
The Fix: link rel=“preload”
<head>
<!-- Preload a standard image -->
<link rel="preload" as="image" href="/images/hero.jpg">
<!-- Preload with format hint -->
<link rel="preload" as="image" href="/images/hero.webp" type="image/webp">
<!-- Preload responsive image -->
<link
rel="preload"
as="image"
href="/images/hero-1200.jpg"
imagesrcset="/images/hero-400.jpg 400w, /images/hero-800.jpg 800w, /images/hero-1200.jpg 1200w"
imagesizes="100vw"
>
</head>
The Fix: fetchpriority=“high”
For images already in HTML (not CSS backgrounds), fetchpriority can be more effective:
<img
src="hero.jpg"
fetchpriority="high"
alt="Hero image"
width="1200"
height="600"
>
When to Use Each
| Scenario | Solution |
|---|---|
LCP is an <img> in HTML | fetchpriority="high" on the img |
LCP is a CSS background-image | <link rel="preload"> in head |
| LCP is loaded by JavaScript | <link rel="preload"> in head |
| LCP image URL is dynamic (CMS) | Generate preload tag server-side |
| LCP is on a CDN with auto-format | Preload with imagesrcset |
Example: Preloading a CDN Image
When using an image CDN like Sirv, preload the base URL and let the CDN handle format negotiation:
<head>
<link
rel="preload"
as="image"
href="https://yoursite.sirv.com/hero.jpg?w=1200"
imagesrcset="
https://yoursite.sirv.com/hero.jpg?w=400 400w,
https://yoursite.sirv.com/hero.jpg?w=800 800w,
https://yoursite.sirv.com/hero.jpg?w=1200 1200w
"
imagesizes="100vw"
>
</head>
Common Mistakes
- Preloading non-LCP images: Only preload the LCP element. Preloading too many resources defeats the purpose.
- Preloading lazy-loaded images: If an image has
loading="lazy", do not preload it. - Mismatched URLs: The preload
hrefmust match the actualsrcexactly, including query parameters. - Missing
as="image": Without this attribute, the browser fetches with wrong priority.
”Avoid Enormous Network Payloads”
What Triggers This Audit
Lighthouse flags pages where total network transfer exceeds 5 MB. Images are almost always the largest contributor.
The exact Lighthouse message:
Avoid enormous network payloads — Large network payloads cost users real money and are highly correlated with long load times.
Image Contribution to Page Weight
On a typical page, images account for 50-80% of total transfer size:
| Page Type | Total Weight | Image Weight | Image Share |
|---|---|---|---|
| News article | 3.2 MB | 2.1 MB | 66% |
| E-commerce product | 4.5 MB | 3.8 MB | 84% |
| Portfolio | 8.2 MB | 7.5 MB | 91% |
| SaaS landing page | 2.8 MB | 1.4 MB | 50% |
Image Budgets
Set budgets based on page type and target audience:
| Page Type | Image Budget | Per-Image Max | Notes |
|---|---|---|---|
| Mobile landing page | 500 KB | 150 KB | Fast 3G users |
| Blog post | 1 MB | 200 KB | Multiple content images |
| Product page | 800 KB | 250 KB | High quality matters |
| Gallery page | 1.5 MB initial | 100 KB/thumb | Lazy load the rest |
| App dashboard | 300 KB | 50 KB | Icons and avatars |
Progressive Loading Strategies
Instead of loading one enormous image, break the experience into stages:
Low-Quality Image Placeholder (LQIP):
<!-- Inline a tiny blurred placeholder, load full image lazily -->
<div class="image-wrapper">
<img
src="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQ..."
data-src="photo-full.jpg"
alt="Photo"
width="800"
height="450"
class="lazy-image"
style="filter: blur(20px); transition: filter 0.3s"
>
</div>
// Replace placeholder with full image
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.onload = () => img.style.filter = 'none';
observer.unobserve(img);
}
});
});
document.querySelectorAll('.lazy-image').forEach(img => observer.observe(img));
Other placeholder strategies include dominant color backgrounds (set background-color on the container to the image’s primary color) and BlurHash (compact blur representations generated at build time).
”Does Not Use Passive Listeners to Improve Scrolling Performance”
Some older lazy loading libraries attach non-passive scroll event listeners, which block smooth scrolling. If your lazy loading uses window.addEventListener('scroll', handler), either add { passive: true } as the third argument, switch to Intersection Observer, or use native loading="lazy" (the best option). Modern lazy loading should never use scroll listeners.
Running Lighthouse Programmatically
Lighthouse CLI
# Install globally
npm install -g lighthouse
# Run audit
lighthouse https://example.com --output=json --output-path=./report.json
# Performance only, with specific categories
lighthouse https://example.com \
--only-categories=performance \
--output=html \
--output-path=./report.html
# Mobile emulation (default)
lighthouse https://example.com --preset=desktop # or default mobile
# With throttling disabled (tests actual connection speed)
lighthouse https://example.com --throttling-method=provided
Node.js API
const lighthouse = require('lighthouse');
const chromeLauncher = require('chrome-launcher');
async function runAudit(url) {
const chrome = await chromeLauncher.launch({ chromeFlags: ['--headless'] });
const result = await lighthouse(url, {
port: chrome.port,
onlyCategories: ['performance'],
output: 'json',
});
// Image-specific audit keys
const imageKeys = [
'uses-responsive-images', 'modern-image-formats',
'uses-optimized-images', 'offscreen-images',
'unsized-images', 'preload-lcp-image',
];
for (const key of imageKeys) {
const audit = result.lhr.audits[key];
if (audit?.score !== null && audit?.score < 1) {
console.log(`[FAIL] ${key}: ${audit.displayValue || 'needs attention'}`);
}
}
await chrome.kill();
return result;
}
runAudit('https://example.com');
Setting Up Lighthouse CI
Performance Budgets for Images
Create a lighthouse-budget.json to enforce image performance:
[
{
"path": "/*",
"resourceSizes": [
{
"resourceType": "image",
"budget": 500
},
{
"resourceType": "total",
"budget": 1500
}
],
"resourceCounts": [
{
"resourceType": "image",
"budget": 25
}
]
},
{
"path": "/blog/*",
"resourceSizes": [
{
"resourceType": "image",
"budget": 800
}
]
}
]
Lighthouse CI Configuration
// lighthouserc.js
module.exports = {
ci: {
collect: {
url: [
'http://localhost:3000/',
'http://localhost:3000/products/',
'http://localhost:3000/blog/sample-post/',
],
numberOfRuns: 3,
},
assert: {
assertions: {
'uses-responsive-images': ['warn', { minScore: 0.9 }],
'modern-image-formats': ['error', { minScore: 0.9 }],
'uses-optimized-images': ['error', { minScore: 0.9 }],
'offscreen-images': ['warn', { minScore: 0.8 }],
'unsized-images': ['error', { minScore: 1 }],
'preload-lcp-image': ['warn', { minScore: 1 }],
'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
'largest-contentful-paint': ['warn', { maxNumericValue: 2500 }],
},
},
upload: {
target: 'temporary-public-storage',
},
},
};
PageSpeed Insights vs Lighthouse
Key Differences
| Aspect | Lighthouse (DevTools/CLI) | PageSpeed Insights |
|---|---|---|
| Data type | Lab data (simulated) | Lab + Field data (CrUX) |
| Device | Your settings | Mobile + Desktop |
| Network | Simulated throttling | Simulated (lab) + Real (field) |
| Consistency | Varies run to run | More stable (field data) |
| API access | Node.js API | REST API |
When to Use Each
- Lighthouse: Development, CI/CD, debugging specific pages
- PageSpeed Insights: Production monitoring, real-user metrics, SEO assessment
- CrUX (Chrome UX Report): Understanding actual user experience across pages
You can also use the PageSpeed Insights REST API to programmatically check image audit scores for any URL, which is useful for monitoring production pages without running a local Lighthouse instance.
Before/After Case Studies
Case Study 1: Blog with 15 Images Per Post
Before:
- All images: 4000px JPEG at quality 95
- No lazy loading
- No width/height attributes
- Performance score: 38
| Audit | Status | Potential Savings |
|---|---|---|
| Properly size images | FAIL | 3.2 MB |
| Next-gen formats | FAIL | 1.8 MB |
| Efficiently encode | FAIL | 900 KB |
| Defer offscreen | FAIL | 4.1 MB |
| Explicit dimensions | FAIL | CLS 0.32 |
Fixes applied:
- Resized images to max 1600px width
- Generated WebP variants, served via
<picture> - Re-encoded JPEGs at quality 82 with MozJPEG
- Added
loading="lazy"to images 3-15 - Added
widthandheightto all images - Added
fetchpriority="high"to hero image
After:
- Performance score: 94
- LCP: 1.8s (was 5.2s)
- CLS: 0.02 (was 0.32)
- Total image weight: 420 KB (was 5.9 MB)
Case Study 2: E-Commerce Product Page
Before:
- 6 product images at 3000x3000 PNG
- Background image loaded via CSS
- No optimization
- Performance score: 29
Fixes applied:
- Switched to Sirv for image hosting with auto-format
- Used URL-based resizing (
?w=800for main,?w=200for thumbnails) - Preloaded the main product image
- Lazy-loaded gallery thumbnails
- Set explicit dimensions on all images
After:
- Performance score: 91
- LCP: 1.4s (was 6.8s)
- CLS: 0 (was 0.18)
- Total image weight: 280 KB (was 12.4 MB)
Case Study 3: Photography Portfolio
A portfolio with 40 full-resolution images on the homepage scored 12 in Performance. After implementing responsive images, converting to WebP/AVIF via Sirv, lazy-loading below the first row, using LQIP placeholders, setting dimensions, and batch-optimizing 200+ images with Sirv AI Studio, the score jumped to 88 with LCP dropping from 12.4s to 2.1s and initial image weight from 18.6 MB to 380 KB.
Image Performance Budget
Setting Budgets
A performance budget defines the maximum resources a page can load. Set image-specific limits:
| Page Type | Total Image Budget | Per-Image Max | Max Image Count |
|---|---|---|---|
| Homepage | 500 KB | 150 KB | 15 |
| Blog post | 800 KB | 200 KB | 20 |
| Product page | 600 KB | 250 KB | 10 |
| Gallery | 400 KB (initial) | 80 KB/thumb | 50 (lazy loaded) |
Monitoring Image Budgets in Production
Use the web-vitals library to track real-user image performance and identify regressions:
import { onLCP, onCLS } from 'web-vitals';
onLCP((metric) => {
const lcpEntry = metric.entries[metric.entries.length - 1];
if (lcpEntry.element?.tagName === 'IMG') {
navigator.sendBeacon('/api/metrics', JSON.stringify({
metric: 'lcp',
value: metric.value,
element: 'image',
url: lcpEntry.element.src,
}));
}
});
Combine this with your Lighthouse CI assertions to catch image performance issues before they reach production, and monitor real-user impact after deployment.
Quick Reference: All Image Audits
| Lighthouse Audit | Category | Fix Summary |
|---|---|---|
| Properly size images | Opportunity | Use responsive srcset and sizes, or a CDN with URL resizing |
| Serve images in next-gen formats | Opportunity | Use <picture> with WebP/AVIF sources, or a CDN with auto-format |
| Efficiently encode images | Opportunity | Re-compress with MozJPEG q80-85, strip metadata |
| Defer offscreen images | Opportunity | Add loading="lazy" to below-fold images |
| Image elements do not have explicit width and height | Diagnostic | Add width and height attributes to every <img> |
| Preload Largest Contentful Paint image | Opportunity | Add <link rel="preload"> or fetchpriority="high" |
| Avoid enormous network payloads | Diagnostic | Set image budgets, compress, and lazy load |
| Uses passive listeners | Diagnostic | Replace scroll listeners with Intersection Observer or native lazy loading |
Every image audit in Lighthouse has a concrete fix. Start with the highest-savings opportunities (usually “Properly size images” and “Serve next-gen formats”), then work through the rest. Using an image CDN like Sirv addresses most of these audits automatically — proper sizing via URL parameters, automatic WebP/AVIF delivery, and global edge caching for fast delivery.