LCP image entropy
Chrome 77+ Edge 79+ Firefox 122+ Safari 26.2+ Details
Checks if images qualify as LCP candidates based on their entropy (bits per pixel). Since Chrome 112 (opens in a new tab), low-entropy images are ignored for LCP measurement.
Based on Stoyan Stefanov’s BPP check (opens in a new tab).
What is BPP (bits per pixel)?
BPP measures image complexity/entropy:
BPP = (file size in bits) / (width × height)
= (encodedBodySize × 8) / (width × height)LCP eligibility threshold
| BPP | Entropy | LCP Eligible | Example |
|---|---|---|---|
| < 0.05 | Low | No | Solid colors, simple gradients, placeholders |
| ≥ 0.05 | Normal | Yes | Photos, complex graphics |
Why this matters: If your largest image has low entropy, Chrome will skip it and use the next largest element for LCP. This can cause unexpected LCP values.
Snippet
Run it from an AI agent: the prompt uses the CLI to measure any URL and report the result.
// LCP Image Entropy Check
// https://webperf-snippets.nucliweb.net
(async () => {
if (!PerformanceObserver.supportedEntryTypes?.includes("largest-contentful-paint")) {
console.warn("⚠️ largest-contentful-paint entries are not supported in this browser.");
return { script: "LCP-Image-Entropy", status: "unsupported", error: "largest-contentful-paint entries not supported in this browser" };
}
// @shared formatBytes
function formatBytes(bytes) {
if (bytes === null || bytes === undefined || Number.isNaN(bytes)) return "-";
if (bytes === 0) return "0 B";
const units = ["B", "KB", "MB", "GB"];
const i = Math.max(0, Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1));
return (bytes / Math.pow(1024, i)).toFixed(1) + " " + units[i];
}
// @end-shared formatBytes
const LCP_THRESHOLD = 0.05; // Chrome's threshold for low-entropy
// Get current LCP element
let lcpElement = null;
let lcpUrl = null;
const lcpObserver = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
if (lastEntry) {
lcpElement = lastEntry.element;
lcpUrl = lastEntry.url;
}
});
lcpObserver.observe({ type: "largest-contentful-paint", buffered: true });
// Wait a tick to ensure LCP is captured (for human console output)
setTimeout(() => {
lcpObserver.disconnect();
// Get all images
const images = [...document.images]
.filter((img) => {
const src = img.currentSrc || img.src;
return src && !src.startsWith("data:image");
})
.map((img) => {
const src = img.currentSrc || img.src;
const resource = performance.getEntriesByName(src)[0];
const fileSize = resource?.encodedBodySize || 0;
const pixels = img.naturalWidth * img.naturalHeight;
const bpp = pixels > 0 ? (fileSize * 8) / pixels : 0;
const isLowEntropy = bpp > 0 && bpp < LCP_THRESHOLD;
const isLCP = lcpElement === img || lcpUrl === src;
return {
element: img,
src,
shortSrc: src.split("/").pop()?.split("?")[0] || src,
width: img.naturalWidth,
height: img.naturalHeight,
fileSize,
bpp,
isLowEntropy,
isLCP,
lcpEligible: !isLowEntropy && bpp > 0,
};
})
.filter((img) => img.bpp > 0); // Only images with measurable BPP
console.group("%c🖼️ Image Entropy Analysis", "font-weight: bold; font-size: 14px;");
if (images.length === 0) {
console.log(" No images with measurable entropy found.");
console.log(" (Data URLs and cross-origin images without CORS are excluded)");
console.groupEnd();
return;
}
// Summary
const lowEntropy = images.filter((img) => img.isLowEntropy);
const normalEntropy = images.filter((img) => !img.isLowEntropy);
const lcpImage = images.find((img) => img.isLCP);
console.log("");
console.log("%cSummary:", "font-weight: bold;");
console.log(` Total images analyzed: ${images.length}`);
console.log(` 🟢 Normal entropy (LCP eligible): ${normalEntropy.length}`);
console.log(` 🔴 Low entropy (LCP ineligible): ${lowEntropy.length}`);
if (lcpImage) {
const icon = lcpImage.isLowEntropy ? "⚠️" : "✅";
console.log("");
console.log(`%c${icon} Current LCP image:`, "font-weight: bold;");
console.log(` ${lcpImage.shortSrc}`);
console.log(` BPP: ${lcpImage.bpp.toFixed(4)} ${lcpImage.isLowEntropy ? "(LOW - may be skipped!)" : "(OK)"}`);
}
// Table
console.log("");
console.log("%cAll Images:", "font-weight: bold;");
const tableData = images
.sort((a, b) => b.bpp - a.bpp)
.map((img) => ({
Image: img.shortSrc.length > 30 ? "..." + img.shortSrc.slice(-27) : img.shortSrc,
Dimensions: `${img.width}×${img.height}`,
Size: formatBytes(img.fileSize),
BPP: img.bpp.toFixed(4),
Entropy: img.isLowEntropy ? "🔴 Low" : "🟢 Normal",
"LCP Eligible": img.lcpEligible ? "✅" : "❌",
"Is LCP": img.isLCP ? "👈" : "",
}));
console.table(tableData);
// Warnings
if (lowEntropy.length > 0) {
console.log("");
console.log("%c⚠️ Low Entropy Images:", "font-weight: bold; color: #f59e0b;");
console.log(" These images will NOT be considered for LCP in Chrome 112+:");
lowEntropy.forEach((img) => {
console.log(` • ${img.shortSrc} (BPP: ${img.bpp.toFixed(4)})`, img.element);
});
}
if (lcpImage && lcpImage.isLowEntropy) {
console.log("");
console.log("%c🚨 Warning:", "font-weight: bold; color: #ef4444;");
console.log(" Your LCP image has low entropy and may be skipped by Chrome!");
console.log(" Chrome will use the next largest element instead.");
console.log("");
console.log("%c💡 Solutions:", "font-weight: bold; color: #3b82f6;");
console.log(" • Replace placeholder with actual content image");
console.log(" • Use a text element as LCP instead");
console.log(" • Ensure hero image loads with sufficient detail");
}
// Elements for inspection
console.log("");
console.log("%c🔎 Inspect elements:", "font-weight: bold;");
images.forEach((img, i) => {
const icon = img.isLowEntropy ? "🔴" : "🟢";
const lcpMark = img.isLCP ? " 👈 LCP" : "";
console.log(` ${i + 1}. ${icon} ${img.shortSrc}${lcpMark}`, img.element);
});
console.groupEnd();
}, 100);
// Return for agent (buffered entries + DOM)
// Chrome only exposes largest-contentful-paint entries through a PerformanceObserver,
// so performance.getEntriesByType() returns [] here. Collect via a buffered observer.
const lcpEntriesSync = await new Promise((resolve) => {
const entries = [];
const obs = new PerformanceObserver((list) => entries.push(...list.getEntries()));
obs.observe({ type: "largest-contentful-paint", buffered: true });
setTimeout(() => { obs.disconnect(); resolve(entries); }, 100);
});
const lcpEntrySync = lcpEntriesSync.at(-1);
const lcpElementSync = lcpEntrySync?.element ?? null;
const lcpUrlSync = lcpEntrySync?.url ?? null;
const imagesSync = [...document.images]
.filter((img) => { const src = img.currentSrc || img.src; return src && !src.startsWith("data:image"); })
.map((img) => {
const src = img.currentSrc || img.src;
const resource = performance.getEntriesByName(src)[0];
const fileSize = resource?.encodedBodySize || 0;
const pixels = img.naturalWidth * img.naturalHeight;
const bpp = pixels > 0 ? (fileSize * 8) / pixels : 0;
const isLowEntropy = bpp > 0 && bpp < LCP_THRESHOLD;
const isLCP = lcpElementSync === img || lcpUrlSync === src;
return {
url: src.split("/").pop()?.split("?")[0] || src,
width: img.naturalWidth,
height: img.naturalHeight,
fileSizeBytes: fileSize,
bpp: Math.round(bpp * 10000) / 10000,
isLowEntropy,
lcpEligible: !isLowEntropy && bpp > 0,
isLCP,
};
})
.filter((img) => img.bpp > 0);
const lowEntropyCount = imagesSync.filter((img) => img.isLowEntropy).length;
const lcpImageSync = imagesSync.find((img) => img.isLCP);
if (lcpElementSync) {
lcpElementSync.style.outline = "3px dashed lime";
lcpElementSync.style.outlineOffset = "2px";
}
const issuesSync = [];
if (lowEntropyCount > 0) {
issuesSync.push({ severity: "warning", message: `${lowEntropyCount} image(s) have low entropy and are LCP-ineligible in Chrome 112+` });
}
if (lcpImageSync?.isLowEntropy) {
issuesSync.push({ severity: "error", message: "Current LCP image has low entropy and may be skipped by Chrome" });
}
// The list keeps the 50 most relevant images (the LCP image, then low entropy, then lowest bits per pixel); count and details cover all of them
const MAX_ITEMS = 50;
const rankedImages = [...imagesSync].sort(
(a, b) => Number(b.isLCP) - Number(a.isLCP) || Number(b.isLowEntropy) - Number(a.isLowEntropy) || a.bpp - b.bpp
);
return {
script: "LCP-Image-Entropy",
status: "ok",
count: imagesSync.length,
details: {
totalImages: imagesSync.length,
lowEntropyCount,
lcpImageEligible: lcpImageSync ? !lcpImageSync.isLowEntropy : null,
lcpImage: lcpImageSync ? { url: lcpImageSync.url, bpp: lcpImageSync.bpp, isLowEntropy: lcpImageSync.isLowEntropy } : null,
},
items: rankedImages.slice(0, MAX_ITEMS),
issues: issuesSync,
};
})();
Understanding the results
Table columns
| Column | Description |
|---|---|
| Image | Filename |
| Dimensions | Width × Height in pixels |
| Size | Encoded file size |
| BPP | Bits per pixel (entropy measure) |
| Entropy | Low (< 0.05) or Normal |
| LCP Eligible | Whether Chrome considers it for LCP |
| Is LCP | 👈 marks the current LCP element |
What causes low entropy
- Solid color images
- Simple gradients
- Loading placeholders (blur-up, LQIP)
- Very small images scaled up
- Highly compressed images with little detail
Common issues
| Issue | Impact | Solution |
|---|---|---|
| Placeholder as LCP | Chrome skips it, unexpected LCP | Load real image faster |
| Gradient hero | Not counted as LCP | Use image with detail or text LCP |
| Lazy-loaded hero | Initial low-res placeholder skipped | Eager load hero image |
Browser support
| Feature | Chrome | Edge | Firefox | Safari |
|---|---|---|---|---|
| LargestContentfulPaint (opens in a new tab) | 77 | 79 | 122 | 26.2 |
| PerformanceObserver (opens in a new tab) | 52 | 79 | 57 | 11 |
| PerformanceResourceTiming.encodedBodySize (opens in a new tab) | 54 | 17 | 45 | 16.4 |
| All of the above | 77 | 79 | 122 | 26.2 |
Source: MDN browser compatibility data (opens in a new tab), version 8.1.4.
Further reading
- LCP change in Chrome 112 (opens in a new tab) | Chromium
- Quick BPP check (opens in a new tab) | Stoyan Stefanov
- LCP Quick Check | Identify your LCP element
Last updated on