# WebPerf Snippets โ Full Reference
A curated collection of JavaScript snippets for web performance measurement.
Each snippet runs in the browser console or Chrome DevTools.
Source: https://webperf-snippets.nucliweb.net
---
# Core Web Vitals
## Cumulative Layout Shift (CLS)
URL: https://webperf-snippets.nucliweb.net/CoreWebVitals/CLS
Quick check for Cumulative Layout Shift, a Core Web Vital that measures visual stability. CLS tracks how much the page layout shifts unexpectedly during its lifetime, providing a single score that represents the cumulative impact of all unexpected layout shifts. Why this matters: Unexpected layout shifts are frustrating and can cause users to click the wrong element or lose their reading position. CLS directly impacts user experience and is a ranking factor for Google Search. Common causes include images without dimensions, web fonts, and dynamically injected content. CLS Rating Thresholds:
```js
// CLS Quick Check
// https://webperf-snippets.nucliweb.net
(async () => {
let cls = 0;
const valueToRating = (score) =>
score <= 0.1 ? "good" : score <= 0.25 ? "needs-improvement" : "poor";
const RATING = {
good: { icon: "๐ข", color: "#0CCE6A" },
"needs-improvement": { icon: "๐ก", color: "#FFA400" },
poor: { icon: "๐ด", color: "#FF4E42" },
};
const logCLS = () => {
const rating = valueToRating(cls);
const { icon, color } = RATING[rating];
console.log(
`%cCLS: ${icon} ${cls.toFixed(4)} (${rating})`,
`color: ${color}; font-weight: bold; font-size: 14px;`
);
};
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
cls += entry.value;
}
}
logCLS();
});
observer.observe({ type: "layout-shift", buffered: true });
// Update on visibility change (final CLS)
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "hidden") {
observer.takeRecords();
console.log("%c๐ Final CLS (on page hide):", "font-weight: bold;");
logCLS();
}
});
// Expose function for manual check
window.getCLS = () => {
logCLS();
const rating = valueToRating(cls);
return {
script: "CLS",
status: "ok",
metric: "CLS",
value: Math.round(cls * 10000) / 10000,
unit: "score",
rating,
thresholds: { good: 0.1, needsImprovement: 0.25 },
};
};
console.log(
" Call %cgetCLS()%c anytime to check current value.",
"font-family: monospace; background: #f3f4f6; padding: 2px 4px;",
""
);
// Return for agent โ collect via buffered observer (getEntriesByType does not
// expose layout-shift entries in Chrome without an active observer).
const clsSync = await new Promise((resolve) => {
let sum = 0;
const obs = new PerformanceObserver((list) => {
for (const e of list.getEntries()) if (!e.hadRecentInput) sum += e.value;
});
obs.observe({ type: "layout-shift", buffered: true });
setTimeout(() => { obs.disconnect(); resolve(sum); }, 100);
});
const clsRating = valueToRating(clsSync);
return {
script: "CLS",
status: "ok",
metric: "CLS",
value: Math.round(clsSync * 10000) / 10000,
unit: "score",
rating: clsRating,
thresholds: { good: 0.1, needsImprovement: 0.25 },
message: "CLS tracking active. Call getCLS() for updated value after page interactions.",
getDataFn: "getCLS",
};
})();
```
---
## Interaction to Next Paint (INP)
URL: https://webperf-snippets.nucliweb.net/CoreWebVitals/INP
Tracks Interaction to Next Paint, a Core Web Vital that measures responsiveness. INP evaluates how quickly a page responds to user interactions throughout the entire page visit, replacing First Input Delay (FID) as a Core Web Vital in March 2024. Why this matters: INP directly measures user frustration. When users click a button and nothing happens for seconds, they abandon tasks. Unlike FID which only measured first interaction, INP tracks ALL interactions, making it a more comprehensive responsiveness metric and a key ranking factor for Google Search. INP Rating Thresholds:
```js
// INP (Interaction to Next Paint) Tracking
// https://webperf-snippets.nucliweb.net
(() => {
const interactions = [];
let inpValue = 0;
let inpEntry = null;
const valueToRating = (ms) =>
ms <= 200 ? "good" : ms <= 500 ? "needs-improvement" : "poor";
const RATING = {
good: { icon: "๐ข", color: "#0CCE6A" },
"needs-improvement": { icon: "๐ก", color: "#FFA400" },
poor: { icon: "๐ด", color: "#FF4E42" },
};
const formatMs = (ms) => `${Math.round(ms)}ms`;
// Calculate INP (98th percentile of all interactions)
const calculateINP = () => {
if (interactions.length === 0) return { value: 0, entry: null };
// Sort by duration
const sorted = [...interactions].sort((a, b) => b.duration - a.duration);
// Get 98th percentile (or worst if < 50 interactions)
const index = interactions.length < 50
? 0
: Math.floor(interactions.length * 0.02);
return {
value: sorted[index].duration,
entry: sorted[index],
};
};
// Format interaction name
const getInteractionName = (entry) => {
const target = entry.target;
if (!target) return entry.name;
let selector = target.tagName.toLowerCase();
if (target.id) selector += `#${target.id}`;
else if (target.className && typeof target.className === "string") {
const classes = target.className.trim().split(/\s+/).slice(0, 2).join(".");
if (classes) selector += `.${classes}`;
}
return `${entry.name} โ ${selector}`;
};
// Get phase breakdown (requires LoAF support)
const getPhaseBreakdown = (entry) => {
const phases = {
inputDelay: 0,
processingTime: 0,
presentationDelay: 0,
};
if (entry.processingStart && entry.processingEnd) {
phases.inputDelay = entry.processingStart - entry.startTime;
phases.processingTime = entry.processingEnd - entry.processingStart;
phases.presentationDelay = entry.duration - phases.inputDelay - phases.processingTime;
}
return phases;
};
// Observer for interactions
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// Only track interactions with interactionId (meaningful interactions)
if (!entry.interactionId) continue;
// Avoid duplicate entries for the same interaction
const existing = interactions.find(
(i) => i.interactionId === entry.interactionId
);
if (!existing || entry.duration > existing.duration) {
// Remove old entry if exists
if (existing) {
const idx = interactions.indexOf(existing);
interactions.splice(idx, 1);
}
interactions.push({
name: entry.name,
duration: entry.duration,
startTime: entry.startTime,
interactionId: entry.interactionId,
target: entry.target,
processingStart: entry.processingStart,
processingEnd: entry.processingEnd,
formattedName: getInteractionName(entry),
phases: getPhaseBreakdown(entry),
entry,
});
}
// Recalculate INP
const result = calculateINP();
inpValue = result.value;
inpEntry = result.entry;
}
});
// Observe event timing
observer.observe({
type: "event",
buffered: true,
durationThreshold: 16, // Only interactions > 16ms (1 frame)
});
// Log INP summary
const logINP = () => {
const rating = valueToRating(inpValue);
const { icon, color } = RATING[rating];
console.group(
`%cINP: ${icon} ${formatMs(inpValue)} (${rating})`,
`color: ${color}; font-weight: bold; font-size: 14px;`
);
console.log("");
console.log(`%c๐ Analysis:`, "font-weight: bold;");
console.log(` Total interactions tracked: ${interactions.length}`);
console.log(` INP (98th percentile): ${formatMs(inpValue)}`);
if (inpEntry) {
console.log("");
console.log(`%c๐ฏ Worst Interaction (INP):`, "font-weight: bold; color: ${color};");
console.log(` Event: ${inpEntry.formattedName}`);
console.log(` Duration: ${formatMs(inpEntry.duration)}`);
// Element attribution
if (inpEntry.target) {
console.log(` Target Element:`, inpEntry.target);
// Get element path for better context
const getElementPath = (el) => {
if (!el) return "";
const parts = [];
let current = el;
while (current && current !== document.body && parts.length < 5) {
let selector = current.tagName.toLowerCase();
if (current.id) selector += `#${current.id}`;
else if (current.className && typeof current.className === "string") {
const classes = current.className.trim().split(/\s+/).slice(0, 2).join(".");
if (classes) selector += `.${classes}`;
}
parts.unshift(selector);
current = current.parentElement;
}
return parts.join(" > ");
};
const path = getElementPath(inpEntry.target);
if (path) {
console.log(` Element Path: ${path}`);
}
}
// Phase breakdown
const phases = inpEntry.phases;
if (phases.inputDelay > 0) {
console.log("");
console.log(`%cโฑ๏ธ Phase Breakdown:`, "font-weight: bold;");
console.log(` Input Delay: ${formatMs(phases.inputDelay)}`);
console.log(` Processing Time: ${formatMs(phases.processingTime)}`);
console.log(` Presentation Delay: ${formatMs(phases.presentationDelay)}`);
// Visual bar
const total = inpEntry.duration;
const barWidth = 40;
const inputBar = "โ".repeat(Math.round((phases.inputDelay / total) * barWidth));
const processBar = "โ".repeat(Math.round((phases.processingTime / total) * barWidth));
const presentBar = "โ".repeat(Math.round((phases.presentationDelay / total) * barWidth));
console.log(` ${inputBar}${processBar}${presentBar}`);
console.log(" โ Input โ Processing โ Presentation");
}
// Recommendations based on phases
if (inpValue > 200 && phases.inputDelay > 0) {
console.log("");
console.log("%c๐ก Recommendations:", "color: #3b82f6; font-weight: bold;");
if (phases.inputDelay > 100) {
console.log(" โข High input delay - Break up long tasks before interaction");
}
if (phases.processingTime > 200) {
console.log(" โข Long processing time - Optimize event handlers");
console.log(" โข Consider debouncing, use requestIdleCallback for non-critical work");
}
if (phases.presentationDelay > 100) {
console.log(" โข High presentation delay - Reduce render complexity");
console.log(" โข Batch DOM updates, use content-visibility");
}
}
}
// Slow interactions breakdown
const slowInteractions = interactions
.filter((i) => i.duration > 200)
.sort((a, b) => b.duration - a.duration)
.slice(0, 10);
if (slowInteractions.length > 0) {
console.log("");
console.log(`%c๐ Slow Interactions (> 200ms):`, "color: #ef4444; font-weight: bold;");
console.table(
slowInteractions.map((i) => ({
Event: i.formattedName,
"Duration (ms)": Math.round(i.duration),
"Start Time (ms)": Math.round(i.startTime),
}))
);
// Show element attribution for top 3
console.log("");
console.log(`%c๐ฏ Element Attribution (top 3):`, "font-weight: bold;");
slowInteractions.slice(0, 3).forEach((interaction, idx) => {
console.log(` ${idx + 1}. ${interaction.formattedName} (${Math.round(interaction.duration)}ms)`);
if (interaction.target) {
console.log(` Element:`, interaction.target);
} else {
console.log(` Element: (no target available)`);
}
});
}
// Interaction types breakdown
const byType = {};
interactions.forEach((i) => {
const type = i.name;
if (!byType[type]) {
byType[type] = { count: 0, totalDuration: 0, maxDuration: 0 };
}
byType[type].count++;
byType[type].totalDuration += i.duration;
byType[type].maxDuration = Math.max(byType[type].maxDuration, i.duration);
});
if (Object.keys(byType).length > 0) {
console.log("");
console.log(`%c๐ By Interaction Type:`, "font-weight: bold;");
console.table(
Object.entries(byType).map(([type, stats]) => ({
Type: type,
Count: stats.count,
"Avg (ms)": Math.round(stats.totalDuration / stats.count),
"Max (ms)": Math.round(stats.maxDuration),
}))
);
}
// General recommendations if no phases available
if (inpValue > 200 && (!inpEntry || !inpEntry.phases || inpEntry.phases.inputDelay === 0)) {
console.log("");
console.log("%c๐ก Recommendations:", "color: #3b82f6; font-weight: bold;");
console.log(" โข Break up long tasks using scheduler.yield() or setTimeout");
console.log(" โข Optimize event handlers - reduce computation time");
console.log(" โข Consider debouncing for frequent events");
console.log(" โข Move heavy work to Web Workers");
console.log(" โข Use requestIdleCallback for non-critical work");
console.log("");
console.log(" Run getINPDetails() for full interaction list");
console.log(" Use Long Animation Frames snippet to identify blocking scripts");
}
console.groupEnd();
};
// Expose function to check INP anytime
window.getINP = () => {
const result = calculateINP();
inpValue = result.value;
inpEntry = result.entry;
logINP();
const rating = valueToRating(inpValue);
const details = { totalInteractions: interactions.length };
if (inpEntry) {
details.worstEvent = inpEntry.formattedName;
details.phases = {
inputDelay: Math.round(inpEntry.phases.inputDelay),
processingTime: Math.round(inpEntry.phases.processingTime),
presentationDelay: Math.round(inpEntry.phases.presentationDelay),
};
}
if (interactions.length === 0) {
return {
script: "INP",
status: "error",
error: "No interactions recorded yet. Interact with the page and call getINP() again.",
getDataFn: "getINP",
details,
};
}
return {
script: "INP",
status: "ok",
metric: "INP",
value: Math.round(inpValue),
unit: "ms",
rating,
thresholds: { good: 200, needsImprovement: 500 },
details,
};
};
// Expose function to get all interactions
window.getINPDetails = () => {
console.group("%c๐ All Interactions Detail", "font-weight: bold; font-size: 14px;");
if (interactions.length === 0) {
console.log(" No interactions recorded yet.");
console.groupEnd();
return [];
}
const sorted = [...interactions].sort((a, b) => b.duration - a.duration);
console.log("");
console.log("%cInteraction Summary:", "font-weight: bold;");
console.table(
sorted.map((i, idx) => ({
"#": idx + 1,
Event: i.formattedName,
"Duration (ms)": Math.round(i.duration),
"Start (ms)": Math.round(i.startTime),
"Input Delay": Math.round(i.phases.inputDelay),
Processing: Math.round(i.phases.processingTime),
Presentation: Math.round(i.phases.presentationDelay),
}))
);
// Show element attribution for all interactions
console.log("");
console.log("%c๐ฏ Element Attribution:", "font-weight: bold;");
const maxToShow = Math.min(sorted.length, 15); // Show up to 15
sorted.slice(0, maxToShow).forEach((interaction, idx) => {
const phases = interaction.phases;
const hasPhases = phases.inputDelay > 0;
console.group(
`${idx + 1}. ${interaction.formattedName} - ${Math.round(interaction.duration)}ms`
);
if (interaction.target) {
console.log("Element:", interaction.target);
// Get element path for better identification
const getPath = (el) => {
if (!el) return "";
const parts = [];
let current = el;
while (current && current !== document.body && parts.length < 5) {
let selector = current.tagName.toLowerCase();
if (current.id) selector += `#${current.id}`;
else if (current.className && typeof current.className === "string") {
const classes = current.className.trim().split(/\s+/).slice(0, 2).join(".");
if (classes) selector += `.${classes}`;
}
parts.unshift(selector);
current = current.parentElement;
}
return parts.join(" > ");
};
const path = getPath(interaction.target);
if (path) {
console.log("Path:", path);
}
} else {
console.log("Element: (no target available)");
}
if (hasPhases) {
console.log(
`Phases: Input ${Math.round(phases.inputDelay)}ms | ` +
`Processing ${Math.round(phases.processingTime)}ms | ` +
`Presentation ${Math.round(phases.presentationDelay)}ms`
);
}
console.groupEnd();
});
if (sorted.length > maxToShow) {
console.log(` ... and ${sorted.length - maxToShow} more interactions`);
}
console.groupEnd();
return sorted;
};
// Log on page hide (final INP)
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "hidden") {
observer.takeRecords();
const result = calculateINP();
inpValue = result.value;
inpEntry = result.entry;
console.log("%c๐ Final INP (on page hide):", "font-weight: bold;");
logINP();
}
});
console.log("%cโก INP Tracking Active", "font-weight: bold; font-size: 14px;");
console.log(" Interactions with duration > 16ms will be tracked.");
console.log(
" Call %cgetINP()%c to see current INP value.",
"font-family: monospace; background: #f3f4f6; padding: 2px 4px;",
""
);
console.log(
" Call %cgetINPDetails()%c for full interaction list.",
"font-family: monospace; background: #f3f4f6; padding: 2px 4px;",
""
);
return {
script: "INP",
status: "tracking",
message: "INP tracking active. Interact with the page then call getINP() for results.",
getDataFn: "getINP",
};
})();
```
---
## LCP Image Entropy
URL: https://webperf-snippets.nucliweb.net/CoreWebVitals/LCP-Image-Entropy
Checks if images qualify as LCP candidates based on their entropy (bits per pixel). Since Chrome 112, low-entropy images are ignored for LCP measurement. Based on Stoyan Stefanov's BPP check. 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:
```js
// LCP Image Entropy Check
// https://webperf-snippets.nucliweb.net
(() => {
const formatBytes = (bytes) => {
if (!bytes) return "-";
const k = 1024;
const sizes = ["B", "KB", "MB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return (bytes / Math.pow(k, i)).toFixed(1) + " " + sizes[i];
};
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);
// Synchronous return for agent (buffered entries + DOM)
const lcpEntriesSync = performance.getEntriesByType("largest-contentful-paint");
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" });
}
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: imagesSync,
issues: issuesSync,
};
})();
```
---
## LCP Subparts
URL: https://webperf-snippets.nucliweb.net/CoreWebVitals/LCP-Subparts
Breaks down Largest Contentful Paint into its four phases to identify optimization opportunities. Understanding which phase is slowest helps you focus your optimization efforts where they'll have the most impact. Based on the Web Vitals Chrome Extension. Why this matters: LCP is a critical Core Web Vital that measures when the largest content element becomes visible. By breaking it into sub-parts, you can pinpoint exactly where delays occur and apply targeted optimizations instead of guessing. The four phases of LCP: ```mermaid flowchart LR A([Navigation
Start]) --> B["โฑ๏ธ TTFB
Server Response"] B --> C["โณ Load Delay
Resource Discovery"] C --> D["๐ฅ Load Time
Download Resource"] D --> E["๐จ Render Delay
Paint Element"] E --> F([โ
LCP
Complete]) style B fill:#FFE5E5,stroke:#FF4444,stroke-width:2px style C fill:#FFF3E5,stroke:#FF8800,stroke-width:2px style D fill:#FFFBE5,stroke:#FFBB00,stroke-width:2px style E fill:#E5FFE5,stroke:#44AA44,stroke-width:2px style F fill:#E5F2FF,stroke:#4488FF,stroke-width:3px ```
```js
// LCP Subparts Analysis
// https://webperf-snippets.nucliweb.net
(async () => {
const formatMs = (ms) => `${Math.round(ms)}ms`;
const formatPercent = (value, total) => `${Math.round((value / total) * 100)}%`;
const valueToRating = (ms) =>
ms <= 2500 ? "good" : ms <= 4000 ? "needs-improvement" : "poor";
const RATING = {
good: { icon: "๐ข", color: "#0CCE6A" },
"needs-improvement": { icon: "๐ก", color: "#FFA400" },
poor: { icon: "๐ด", color: "#FF4E42" },
};
const SUB_PARTS = [
{ name: "Time to First Byte", key: "ttfb", target: 800 },
{ name: "Resource Load Delay", key: "resourceLoadDelay", targetPercent: 10 },
{ name: "Resource Load Time", key: "resourceLoadTime", targetPercent: 40 },
{ name: "Element Render Delay", key: "elementRenderDelay", targetPercent: 10 },
];
const getNavigationEntry = () => {
const navEntry = performance.getEntriesByType("navigation")[0];
if (navEntry?.responseStart > 0 && navEntry.responseStart < performance.now()) {
return navEntry;
}
return null;
};
const observer = new PerformanceObserver((list) => {
const lcpEntry = list.getEntries().at(-1);
if (!lcpEntry) return;
const navEntry = getNavigationEntry();
if (!navEntry) return;
const lcpResEntry = performance
.getEntriesByType("resource")
.find((e) => e.name === lcpEntry.url);
const activationStart = navEntry.activationStart || 0;
// Calculate sub-parts
const ttfb = Math.max(0, navEntry.responseStart - activationStart);
const lcpRequestStart = Math.max(
ttfb,
lcpResEntry
? (lcpResEntry.requestStart || lcpResEntry.startTime) - activationStart
: 0
);
const lcpResponseEnd = Math.max(
lcpRequestStart,
lcpResEntry ? lcpResEntry.responseEnd - activationStart : 0
);
const lcpRenderTime = Math.max(
lcpResponseEnd,
lcpEntry.startTime - activationStart
);
const subPartValues = {
ttfb: ttfb,
resourceLoadDelay: lcpRequestStart - ttfb,
resourceLoadTime: lcpResponseEnd - lcpRequestStart,
elementRenderDelay: lcpRenderTime - lcpResponseEnd,
};
// LCP Rating
const rating = valueToRating(lcpRenderTime);
const { icon, color } = RATING[rating];
console.group(
`%cLCP: ${icon} ${formatMs(lcpRenderTime)} (${rating})`,
`color: ${color}; font-weight: bold; font-size: 14px;`
);
// Element info
if (lcpEntry.element) {
const el = lcpEntry.element;
let selector = el.tagName.toLowerCase();
if (el.id) selector = `#${el.id}`;
else if (el.className && typeof el.className === "string") {
const classes = el.className.trim().split(/\s+/).slice(0, 2).join(".");
if (classes) selector = `${el.tagName.toLowerCase()}.${classes}`;
}
console.log("");
console.log("%cLCP Element:", "font-weight: bold;");
console.log(` ${selector}`, el);
if (lcpEntry.url) {
const shortUrl = (() => {
try {
const u = new URL(lcpEntry.url);
return u.hostname !== location.hostname
? `${u.hostname}/โฆ/${u.pathname.split("/").pop()?.split("?")[0]}`
: u.pathname.split("/").pop()?.split("?")[0] || lcpEntry.url;
} catch { return lcpEntry.url; }
})();
console.log(` URL: ${shortUrl}`);
}
// Highlight
el.style.outline = "3px dashed lime";
el.style.outlineOffset = "2px";
}
// Sub-parts table
console.log("");
console.log("%cSub-Parts Breakdown:", "font-weight: bold;");
// Find the slowest phase
const phases = SUB_PARTS.map((part) => ({
...part,
value: subPartValues[part.key],
percent: (subPartValues[part.key] / lcpRenderTime) * 100,
}));
const slowest = phases.reduce((a, b) => (a.value > b.value ? a : b));
const tableData = phases.map((part) => {
const isSlowest = part.key === slowest.key;
const isOverTarget = part.target
? part.value > part.target
: part.percent > part.targetPercent;
return {
"Sub-part": isSlowest ? `โ ๏ธ ${part.name}` : part.name,
Time: formatMs(part.value),
"%": formatPercent(part.value, lcpRenderTime),
Status: isOverTarget ? "๐ด Over target" : "โ
OK",
};
});
console.table(tableData);
// Visual bar
const barWidth = 40;
const bars = phases.map((p) => {
const width = Math.max(1, Math.round((p.value / lcpRenderTime) * barWidth));
return { key: p.key, bar: width };
});
const ttfbBar = "โ".repeat(bars[0].bar);
const delayBar = "โ".repeat(bars[1].bar);
const loadBar = "โ".repeat(bars[2].bar);
const renderBar = "โ".repeat(bars[3].bar);
console.log("");
console.log(` ${ttfbBar}${delayBar}${loadBar}${renderBar}`);
console.log(" โ TTFB โ Load Delay โ Load Time โ Render Delay");
// Recommendations based on slowest phase
console.log("");
console.log("%c๐ก Optimization Focus:", "font-weight: bold; color: #3b82f6;");
console.log(` Slowest phase: ${slowest.name} (${formatPercent(slowest.value, lcpRenderTime)})`);
if (slowest.key === "ttfb") {
console.log(" โ Use a CDN to reduce latency");
console.log(" โ Enable server-side caching");
console.log(" โ Optimize server response time");
} else if (slowest.key === "resourceLoadDelay") {
console.log(" โ Preload the LCP image: ");
console.log(" โ Remove render-blocking resources");
console.log(" โ Inline critical CSS");
} else if (slowest.key === "resourceLoadTime") {
console.log(" โ Compress and resize the LCP image");
console.log(" โ Use modern formats (WebP, AVIF)");
console.log(" โ Use a CDN for faster delivery");
} else if (slowest.key === "elementRenderDelay") {
console.log(" โ Reduce render-blocking JavaScript");
console.log(" โ Avoid client-side rendering for LCP element");
console.log(" โ Use fetchpriority=\"high\" on LCP image");
}
// Performance entries for DevTools
SUB_PARTS.forEach((part) => performance.clearMeasures(part.name));
phases.forEach((part) => {
const startTimes = {
ttfb: 0,
resourceLoadDelay: ttfb,
resourceLoadTime: lcpRequestStart,
elementRenderDelay: lcpResponseEnd,
};
performance.measure(part.name, {
start: startTimes[part.key],
end: startTimes[part.key] + part.value,
});
});
console.log("");
console.log("%c๐ Measures added to Performance timeline", "color: #666;");
console.log(" Open DevTools โ Performance โ reload to see waterfall");
console.groupEnd();
});
observer.observe({ type: "largest-contentful-paint", buffered: true });
console.log("%c๐ LCP Subparts Analysis Active", "font-weight: bold; font-size: 14px;");
console.log(" Waiting for LCP...");
// Return for agent โ collect via buffered observer (getEntriesByType does not
// expose largest-contentful-paint entries in Chrome without an active observer).
const lcpEntry = 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.at(-1) ?? null); }, 100);
});
if (!lcpEntry) {
return { script: "LCP-Subparts", status: "error", error: "No LCP entries buffered" };
}
const navEntrySync = getNavigationEntry();
if (!navEntrySync) {
return { script: "LCP-Subparts", status: "error", error: "No navigation entry" };
}
const lcpResEntrySync = performance.getEntriesByType("resource")
.find((e) => e.name === lcpEntry.url);
const activationStartSync = navEntrySync.activationStart || 0;
const ttfbSync = Math.max(0, navEntrySync.responseStart - activationStartSync);
const lcpRequestStartSync = Math.max(ttfbSync,
lcpResEntrySync ? (lcpResEntrySync.requestStart || lcpResEntrySync.startTime) - activationStartSync : 0
);
const lcpResponseEndSync = Math.max(lcpRequestStartSync,
lcpResEntrySync ? lcpResEntrySync.responseEnd - activationStartSync : 0
);
const lcpRenderTimeSync = Math.max(lcpResponseEndSync, lcpEntry.startTime - activationStartSync);
const totalSync = Math.round(lcpRenderTimeSync);
const ratingSync = valueToRating(totalSync);
const ttfbVal = Math.round(ttfbSync);
const loadDelayVal = Math.round(lcpRequestStartSync - ttfbSync);
const loadTimeVal = Math.round(lcpResponseEndSync - lcpRequestStartSync);
const renderDelayVal = Math.round(lcpRenderTimeSync - lcpResponseEndSync);
const subPartsForRank = [
{ key: "ttfb", value: ttfbVal },
{ key: "resourceLoadDelay", value: loadDelayVal },
{ key: "resourceLoadTime", value: loadTimeVal },
{ key: "elementRenderDelay", value: renderDelayVal },
];
const slowestPhaseSync = subPartsForRank.reduce((a, b) => a.value > b.value ? a : b).key;
let lcpSelectorSync = null;
if (lcpEntry.element) {
const el = lcpEntry.element;
lcpSelectorSync = el.tagName.toLowerCase();
if (el.id) lcpSelectorSync = `#${el.id}`;
else if (el.className && typeof el.className === "string") {
const classes = el.className.trim().split(/\s+/).slice(0, 2).join(".");
if (classes) lcpSelectorSync = `${el.tagName.toLowerCase()}.${classes}`;
}
}
const shortUrlSync = lcpEntry.url
? (() => {
try {
const u = new URL(lcpEntry.url);
return u.hostname !== location.hostname
? `${u.hostname}/โฆ/${u.pathname.split("/").pop()?.split("?")[0]}`
: u.pathname.split("/").pop()?.split("?")[0] || lcpEntry.url;
} catch { return lcpEntry.url; }
})()
: null;
return {
script: "LCP-Subparts",
status: "ok",
metric: "LCP",
value: totalSync,
unit: "ms",
rating: ratingSync,
thresholds: { good: 2500, needsImprovement: 4000 },
details: {
element: lcpSelectorSync,
url: shortUrlSync,
subParts: {
ttfb: { value: ttfbVal, percent: Math.round((ttfbVal / totalSync) * 100), overTarget: ttfbVal > 800 },
resourceLoadDelay: { value: loadDelayVal, percent: Math.round((loadDelayVal / totalSync) * 100), overTarget: (loadDelayVal / totalSync) * 100 > 10 },
resourceLoadTime: { value: loadTimeVal, percent: Math.round((loadTimeVal / totalSync) * 100), overTarget: (loadTimeVal / totalSync) * 100 > 40 },
elementRenderDelay: { value: renderDelayVal, percent: Math.round((renderDelayVal / totalSync) * 100), overTarget: (renderDelayVal / totalSync) * 100 > 10 },
},
slowestPhase: slowestPhaseSync,
},
};
})();
```
---
## LCP Trail
URL: https://webperf-snippets.nucliweb.net/CoreWebVitals/LCP-Trail
Tracks every LCP candidate element during page load and highlights each one with a distinct pastel-colored dashed outline โ so you can see the full trail from first candidate to final LCP. Each time the browser promotes a larger element as the new LCP, the snippet assigns the next color in the palette and highlights it. All previous candidates remain visible on the page for easy comparison.
```js
// LCP Trail
// Tracks all LCP candidate elements during page load
// https://webperf-snippets.nucliweb.net
(() => {
const PALETTE = [
{ color: "#EF4444", name: "Red" },
{ color: "#F97316", name: "Orange" },
{ color: "#22C55E", name: "Green" },
{ color: "#3B82F6", name: "Blue" },
{ color: "#A855F7", name: "Purple" },
{ color: "#EC4899", name: "Pink" },
];
const valueToRating = (ms) =>
ms <= 2500 ? "good" : ms <= 4000 ? "needs-improvement" : "poor";
const RATING = {
good: { icon: "๐ข", color: "#0CCE6A" },
"needs-improvement": { icon: "๐ก", color: "#FFA400" },
poor: { icon: "๐ด", color: "#FF4E42" },
};
const getActivationStart = () => {
const navEntry = performance.getEntriesByType("navigation")[0];
return navEntry?.activationStart || 0;
};
const getSelector = (element) => {
if (element.id) return `#${element.id}`;
if (element.className && typeof element.className === "string") {
const classes = element.className.trim().split(/\s+/).slice(0, 2).join(".");
if (classes) return `${element.tagName.toLowerCase()}.${classes}`;
}
return element.tagName.toLowerCase();
};
const getElementInfo = (element, entry) => {
const tag = element.tagName.toLowerCase();
if (tag === "img") return { type: "Image", url: entry.url || element.src };
if (tag === "video") return { type: "Video poster", url: entry.url || element.poster };
if (window.getComputedStyle(element).backgroundImage !== "none") return { type: "Background image", url: entry.url };
return { type: tag === "h1" || tag === "p" ? "Text block" : tag };
};
const candidates = [];
const logTrail = () => {
const current = candidates[candidates.length - 1];
if (!current) return;
const rating = valueToRating(current.time);
const { icon, color: ratingColor } = RATING[rating];
console.group(
`%cLCP: ${icon} ${(current.time / 1000).toFixed(2)}s (${rating})`,
`color: ${ratingColor}; font-weight: bold; font-size: 14px;`
);
// Current LCP element attribution
console.log("");
console.log("%cLCP Element:", "font-weight: bold;");
console.log(` Element: ${current.selector}`, current.element);
const { type, url } = getElementInfo(current.element, current.entry);
console.log(` Type: ${type}`);
if (url) console.log(` URL: ${url}`);
if (current.element.naturalWidth) {
console.log(
` Dimensions: ${current.element.naturalWidth}ร${current.element.naturalHeight}`
);
}
if (current.entry.size) {
console.log(` Size: ${current.entry.size.toLocaleString()} pxยฒ`);
}
// Trail legend
console.log("");
console.log("%cCandidates Trail:", "font-weight: bold;");
candidates.forEach(({ index, selector, color, name, time, element }) => {
const isCurrent = index === candidates.length;
console.log(
`%c โ ${index}. ${selector}`,
`color: ${color}; font-weight: ${isCurrent ? "bold" : "normal"};`,
`| ${(time / 1000).toFixed(2)}s โ ${name}${isCurrent ? " โ LCP" : ""}`,
element
);
});
console.log("");
console.log(
"%cโ Each candidate highlighted with a colored dashed outline",
"color: #22c55e;"
);
console.groupEnd();
};
const observer = new PerformanceObserver((list) => {
const activationStart = getActivationStart();
const seen = new Set(candidates.map((c) => c.element));
for (const entry of list.getEntries()) {
const { element } = entry;
if (!element || seen.has(element)) continue;
const { color, name } = PALETTE[candidates.length % PALETTE.length];
element.style.outline = `3px dashed ${color}`;
element.style.outlineOffset = "2px";
candidates.push({
index: candidates.length + 1,
element,
selector: getSelector(element),
color,
name,
time: Math.max(0, entry.startTime - activationStart),
entry,
});
seen.add(element);
}
logTrail();
});
observer.observe({ type: "largest-contentful-paint", buffered: true });
console.log("%cโฑ๏ธ LCP Trail Active", "font-weight: bold; font-size: 14px;");
console.log(" Highlights all LCP candidate elements with distinct colors.");
// Synchronous return for agent (buffered entries)
const trailEntries = performance.getEntriesByType("largest-contentful-paint");
if (trailEntries.length === 0) {
return { script: "LCP-Trail", status: "error", error: "No LCP entries yet" };
}
const trailActivationStart = getActivationStart();
const seenEls = new Set();
const syncCandidates = [];
for (const entry of trailEntries) {
const el = entry.element;
if (!el || seenEls.has(el)) continue;
seenEls.add(el);
const selector = getSelector(el);
const time = Math.round(Math.max(0, entry.startTime - trailActivationStart));
const { type, url } = getElementInfo(el, entry);
syncCandidates.push({
index: syncCandidates.length + 1,
selector,
time,
elementType: type,
...(url ? {
url: (() => {
try {
const u = new URL(url);
return u.hostname !== location.hostname
? `${u.hostname}/โฆ/${u.pathname.split("/").pop()?.split("?")[0]}`
: u.pathname.split("/").pop()?.split("?")[0] || url;
} catch { return url; }
})(),
} : {}),
});
}
if (syncCandidates.length === 0) {
return { script: "LCP-Trail", status: "error", error: "No LCP elements in DOM" };
}
const lastCandidate = syncCandidates.at(-1);
const trailValue = lastCandidate.time;
const trailRating = valueToRating(trailValue);
return {
script: "LCP-Trail",
status: "ok",
metric: "LCP",
value: trailValue,
unit: "ms",
rating: trailRating,
thresholds: { good: 2500, needsImprovement: 4000 },
details: {
candidateCount: syncCandidates.length,
finalElement: lastCandidate.selector,
candidates: syncCandidates,
},
};
})();
```
---
## LCP Video Candidate
URL: https://webperf-snippets.nucliweb.net/CoreWebVitals/LCP-Video-Candidate
Detects whether the LCP element is a `