Skip to Content
Core Web VitalsLCP Image Entropy

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

BPPEntropyLCP EligibleExample
< 0.05 Low NoSolid colors, simple gradients, placeholders
≥ 0.05 Normal YesPhotos, 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

ColumnDescription
ImageFilename
DimensionsWidth × Height in pixels
SizeEncoded file size
BPPBits per pixel (entropy measure)
Entropy Low (< 0.05) or Normal
LCP EligibleWhether 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

IssueImpactSolution
Placeholder as LCPChrome skips it, unexpected LCPLoad real image faster
Gradient heroNot counted as LCPUse image with detail or text LCP
Lazy-loaded heroInitial low-res placeholder skippedEager load hero image

Browser support

FeatureChromeEdgeFirefoxSafari
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

Last updated on
MIT 2026 © Joan León | @nucliweb