Core Web Vitals - Analysing and Improving Web applications
What is Core WebVitals? #
Core web vitals assess the speed, interactivity, and visual stability of a website or application. These metrics are crucial for SEO as they significantly impact search result rankings and user experience. Enhancing core web vitals can lead to improved site visibility and better engagement for users.
The Core web vitals are :
Largest Contentful Paint (LCP): It measures the timing for the browser to render the largest text or the image which is blocked in view.
Cumulative Layout Shift (CLS): It measures the visual stability of a webpage by tracking how much content moves unexpectedly during loading. A high CLS score indicates poor user experience, as it can cause users to lose their place or click on wrong elements. Minimizing layout shifts ensures a more stable and user-friendly interface.
Interaction to Next Paint (INP): It measures the website responsiveness. It tracks how long it takes for the page to visually respond to the user actions (click, taps etc.). A lower INP indicates a faster and more responsive user experience.
What Is Largest Contentful Paint (LCP)? #
Ways to improve the LCP
Setup lazy loading
Minify the CSS
Minify the JS
Use caching
Optimize Server Response Time
Eliminate or Defer Third-party Scripts
Minimize Render-blocking Resources
Compress and Optimize Images: Use compressed images and optimize them for the web to reduce their file sizes without compromising quality. This can significantly improve loading times, especially for pages with many images. To optimize the image we can load different images in different dimensions.
<picture>
<source srcset="your-path.avif" type="image/avif" />
<source srcset="your-path.webp" type="image/webp" />
<source srcset="your-path.jpg" type="image/jpeg" />
<img
src="your-path.jpg"
alt="A description of the image."
width="100"
height="100"
loading="lazy"
decoding="async"
/>
</picture>
<!-- For example if we have 2 image one for mobile and one for desktop we will render it like -->
<picture>
<source media="(max-width: 767.98px)" srcset="your-smaller-device-image-path.png" sizes="100vw" />
<source media="(min-width: 768px)" srcset="your-default-image-path.png" sizes="100vw" />
<img
src="your-default-image-path.png"
alt="A description of the image."
width="100"
height="100"
loading="lazy"
decoding="async"
/>
</picture>
<!-- For example if we have 2 image one for mobile and one for desktop but with different resolutions we will render it like -->
<picture>
<source
media="(max-width: 767.98px)"
srcset="
your-smaller-device-image-path-250w.png 250w,
your-smaller-device-image-path-350w.png 350w,
your-smaller-device-image-path-500w.png 500w,
your-smaller-device-image-path-700w.png 700w
"
sizes="100vw"
/>
<source
media="(min-width: 768px)"
srcset="
your-default-image-path-1380w.png 1380w,
your-default-image-path-1500w.png 1500w,
your-default-image-path-2500w.png 2500w
"
sizes="100vw"
/>
<img
src="your-default-image-path.png"
alt="A description of the image."
width="100"
height="100"
loading="lazy"
decoding="async"
/>
</picture>
There are other several ways to improve LCP score which google provides us with the ability to optimize the performance
We can also monitor the LCP in JavaScript directly using the console. Refer here LCP breakdown in JavaScript for more information and example usage below.
$(function () {
const LCP_SUB_PARTS = ["TTFB (Time to first byte)", "Load delay", "Load time", "Render delay"];
function formatTime(milliseconds) {
const seconds = milliseconds / 1000;
return seconds.toFixed(2) + "s";
}
function getLCPStatus(lcpTime) {
if (lcpTime <= 2500) {
return "Good";
} else if (lcpTime <= 4000) {
return "Needs Improvement";
} else {
return "Poor";
}
}
new PerformanceObserver((list) => {
const lcpEntry = list.getEntries().at(-1);
const navEntry = performance.getEntriesByType("navigation")[0];
const lcpResEntry = performance
.getEntriesByType("resource")
.filter((e) => e.name === lcpEntry.url)[0];
if (!lcpEntry.url) return;
const ttfb = navEntry.responseStart;
const lcpRequestStart = Math.max(
ttfb,
lcpResEntry ? lcpResEntry.requestStart || lcpResEntry.startTime : 0
);
const lcpResponseEnd = Math.max(lcpRequestStart, lcpResEntry ? lcpResEntry.responseEnd : 0);
const lcpRenderTime = Math.max(lcpResponseEnd, lcpEntry ? lcpEntry.startTime : 0);
LCP_SUB_PARTS.forEach((part) => performance.clearMeasures(part));
const lcpSubPartMeasures = [
performance.measure(LCP_SUB_PARTS[0], {
start: 0,
end: ttfb,
}),
performance.measure(LCP_SUB_PARTS[1], {
start: ttfb,
end: lcpRequestStart,
}),
performance.measure(LCP_SUB_PARTS[2], {
start: lcpRequestStart,
end: lcpResponseEnd,
}),
performance.measure(LCP_SUB_PARTS[3], {
start: lcpResponseEnd,
end: lcpRenderTime,
}),
];
const lcpStatus = getLCPStatus(lcpRenderTime);
const tableData = lcpSubPartMeasures.map((measure) => ({
"LCP Phase": measure.name,
"% of LCP": `${Math.round((1000 * measure.duration) / lcpRenderTime) / 10}%`,
Timing: formatTime(measure.duration),
}));
tableData.push({
"LCP Phase": "TOTAL",
"% of LCP": "100%",
Timing: formatTime(lcpRenderTime),
Status: lcpStatus,
});
console.table(tableData);
console.log("LCP element: ", lcpEntry.element, lcpEntry.url);
}).observe({
type: "largest-contentful-paint",
buffered: true,
});
});
What Is Cumulative Layout Shift (CLS)? #
It measures the visual stability of a webpage by tracking how much content moves unexpectedly during loading. A high CLS score indicates poor user experience, as it can cause users to lose their place or click on wrong elements. Minimizing layout shifts ensures a more stable and user-friendly interface.
Importance of CLS :
User engagement
Accessibility
SEO and Ranking
Ways to improve the CLS :
Common Issue and fixes
Images, iFrames or video’s - setting
height
andwidth
to elementsWeb fonts - Preloading Fonts
Slow interactions
What Is Interaction to Next Paint (INP)? #
INP is a metric that evaluates a webpage’s responsiveness to user interactions by measuring the latency of all clicks, taps, and keyboard actions during a user’s visit. The final INP value represents the longest interaction recorded, excluding any outliers.
How it is calculated ? #
INP is calculated by monitoring all interactions with a webpage. For most sites, the interaction with the highest latency is reported as the INP.
Ways to improve the INP
Optimize long tasks
Optimize Input delay
Use simple CSS selectors instead of very complex selectors
Refer here INP breakdown in JavaScript for more information and example usage below and other (additional information | INP Blog)
$(function() {
const RATING_COLORS = {
good: "#0CCE6A",
"needs-improvement": "#FFA400",
poor: "#FF4E42"
};
function onInteraction(callback) {
const valueToRating = (score) =>
score <= 200 ? "good" : score <= 500 ? "needs-improvement" : "poor";
const observer = new PerformanceObserver((list) => {
const interactions = {};
for (let entry of list
.getEntries()
.filter((entry) => entry.interactionId)) {
interactions[entry.interactionId] =
interactions[entry.interactionId] || [];
interactions[entry.interactionId].push(entry);
}
for (let interaction of Object.values(interactions)) {
const entry = interaction.reduce((prev, curr) =>
prev.duration >= curr.duration ? prev : curr
);
const entryTarget = interaction.map(entry => entry.target).find(target => !!target);
const value = entry.duration;
callback({
attribution: {
eventEntry: entry,
eventTarget: entryTarget,
eventTime: entry.startTime,
eventType: entry.name
},
entries: interaction,
name: "Interaction",
rating: valueToRating(value),
value
});
}
});
observer.observe({
type: "event",
durationThreshold: 0,
buffered: true
});
}
function logInteraction(interaction) {
console.log(
[${interaction.name}] %c${interaction.value.toFixed(0)}ms (${interaction.rating}),
color: ${RATING_COLORS[interaction.rating] || "inherit"}
);
for (let entry of interaction.entries) {
console.log(
Interaction event type: %c${entry.name},
"font-family: monospace"
);
console.log("Interaction target:", interaction.attribution.eventTarget);
const adjustedPresentationTime = Math.max(
entry.processingEnd,
entry.startTime + entry.duration
);
console.table([{
subPartString: "Input delay",
"Time(ms)": Math.round(entry.processingStart - entry.startTime, 0)
},
{
subPartString: "Processing time",
"Time(ms)": Math.round(entry.processingEnd - entry.processingStart, 0)
},
{
subPartString: "Presentation delay",
"Time(ms)": Math.round(
adjustedPresentationTime - entry.processingEnd,
0
)
}
]);
}
}
function logAllInteractions() {
onInteraction(logInteraction);
}
logAllInteractions();
});