Magento Core Web Vitals — The Actual LCP, INP & CLS Recipe
Four real diagnoses from production Magento + Hyvä stores in 2026. LCP element pinned in DevTools Performance (usually the hero image or an H1 stuck in font-swap), a 10 KiB inline critical CSS budget, a 1200w / 800w / 480w WebP <picture> matrix, INP traced to a 3-second Knockout boot on Luma checkouts, and CLS killed by reserving min-height on the cookie banner. Real Lighthouse JSON excerpts, a recipe checklist, and the mobile P=52 → P=88 numbers that came out the other side.
Magento Core Web Vitals are the three Google-defined performance metrics — Largest Contentful Paint (LCP), Interaction to Next Paint (INP), and Cumulative Layout Shift (CLS) — that govern search ranking and conversion on a Magento 2.4.4 — 2.4.9 storefront on Luma, Hyvä, or a headless front end. The fix is a four-step recipe that targets the actual offender per metric instead of chasing the aggregate Lighthouse score.
How we hit this
Of the 23 Magento performance sprints we ran between September 2025 and May 2026, every single one started with the same mobile profile: P=48–58, LCP 3.8–5.2s, INP 320–480ms, CLS 0.18–0.31. The merchants had all tried "turn on minification" and "enable Varnish" and were still red across the board.
Lighthouse tells you the score. It does not tell you which DOM node is bleeding 2.4 seconds of LCP. The DevTools Performance tab does — start there.
1. LCP: find the bleeding element first
LCP is dominated by one DOM node — usually the hero image, sometimes the H1, occasionally a Magewire-rendered product block. Lighthouse prints the selector; the DevTools Performance tab gives the paint timing breakdown that tells you whether the element is late because of network (image weight), CSS (render-blocking stylesheet), or font (web-font swap).
Diagnostic
npm install -g lighthouse
lighthouse https://store.example.com/ \
--preset=mobile \
--output=json \
--output-path=./lh-before.json \
--only-categories=performance
# Pull the LCP element
jq '.audits["largest-contentful-paint-element"].details.items[0]' lh-before.jsonTypical output on a Magento + Hyvä storefront before the fix:
{
"node": {
"type": "node",
"path": "1,HTML,1,BODY,2,MAIN,0,SECTION,0,DIV,0,PICTURE,1,IMG",
"selector": "main > section > div > picture > img",
"nodeLabel": "img",
"snippet": "<img src=\"/media/wysiwyg/home/hero.jpg\" alt=\"Spring sale\" loading=\"lazy\">"
},
"phase": "loadTime"
}Three red flags: loading="lazy" on an LCP element, no fetchpriority="high", and a full-resolution JPEG served to a 360px mobile viewport.
Fix: image weight matrix + preload + critical CSS
The image weight matrix is the highest-leverage fix on Magento hero imagery. Serve three sizes (1200w / 800w / 480w) as WebP via a <picture>, drop the lazy attribute, add fetchpriority="high", and preload the smallest variant in <head>.
<link rel="preload"
as="image"
href="/media/wysiwyg/home/hero-480.webp"
imagesrcset="/media/wysiwyg/home/hero-480.webp 480w,
/media/wysiwyg/home/hero-800.webp 800w,
/media/wysiwyg/home/hero-1200.webp 1200w"
imagesizes="100vw"
fetchpriority="high">
<picture>
<source type="image/webp"
srcset="/media/wysiwyg/home/hero-480.webp 480w,
/media/wysiwyg/home/hero-800.webp 800w,
/media/wysiwyg/home/hero-1200.webp 1200w"
sizes="100vw">
<img src="/media/wysiwyg/home/hero-800.jpg"
alt="Spring sale"
width="1200" height="600"
fetchpriority="high"
decoding="async">
</picture>Generate the three sizes with cwebp at deploy, not per request:
for w in 480 800 1200; do
cwebp -q 78 -resize $w 0 pub/media/wysiwyg/home/hero.jpg \
-o pub/media/wysiwyg/home/hero-$w.webp
doneThe critical CSS budget is the second lever. Hyvä's styles.css is ~80 KiB minified; Luma's styles-m.css is ~200 KiB. Both block render. Inline a 10 KiB hand-curated subset for above-the-fold layout, preload the rest, async-load.
<style>
/* 10 KiB hand-curated critical CSS — header, hero, primary CTA, font-face */
/* ... */
</style>
<link rel="preload" href="/static/version1716192000/frontend/Vendor/theme/en_US/css/styles.css" as="style">
<link rel="stylesheet" href="/static/version1716192000/frontend/Vendor/theme/en_US/css/styles.css" media="print" onload="this.media='all'">The 10 KiB is not arbitrary — HTTP/2 ships ~14 KiB in the first TCP round trip; 10 KiB leaves room for the HTML shell. Past 14 KiB inline gives no LCP win.
Web-font swap on the H1
When the H1 is the LCP element (text-heavy category, blog, glossary pages), the delay is almost always font-display: block holding the text invisible while the web font downloads. Set font-display: swap on every @font-face and preload the .woff2 with crossorigin.
@font-face {
font-family: 'Inter';
src: url('/static/.../inter-var.woff2') format('woff2');
font-weight: 100 900;
font-display: swap;
}2. INP: the Knockout boot delay on Luma checkouts
INP failures on Magento are dominated by one pattern — Knockout.js + RequireJS booting the checkout page blocks the main thread for 2.8 to 3.4 seconds. Any tap on a payment radio during that window registers as an INP event with a 3-second delay.
Diagnostic
Open DevTools Performance with CPU 4x and Network "Fast 4G". Record a tap on the payment radio. Look for the long task that includes Magento_Checkout/js/view/payment.
jq '.audits["interaction-to-next-paint"].details.items[0]' lh-before.json{
"interactionType": "pointerdown",
"nodeLabel": "input.radio.payment-method",
"selector": "#checkout-step-payment input[name='payment[method]']",
"duration": 412,
"presentationDelay": 38,
"inputDelay": 318,
"processingDuration": 56
}The 318ms inputDelay is the smoking gun — the browser was parsing Knockout templates when the tap arrived.
Fix on Luma: code-split the checkout
Magento's checkout/onepage ships ~1.8 MB of compiled RequireJS to render a form that is mostly conditional. Two-step fix: move analytics, chat, and review-fetch out of the synchronous bundle and defer them on requestIdleCallback; disable Knockout components not visible at first paint (Adobe's Magento_Checkout ships ~30, the customer sees ~6).
// Magento_Checkout/web/js/view/checkout-defer.js
define(['uiComponent'], function (Component) {
return Component.extend({
initialize: function () {
this._super();
if ('requestIdleCallback' in window) {
requestIdleCallback(() => this.bootDeferredViews(), { timeout: 2500 });
} else {
setTimeout(() => this.bootDeferredViews(), 1500);
}
return this;
},
bootDeferredViews: function () {
// Lazy-init review summary, gift-card form, order comment, newsletter opt-in.
}
});
});Fix on Hyvä: defer Alpine.js below the fold
Hyvä's Alpine.js is an order of magnitude faster than Luma's Knockout, but a chatty Magewire cart drawer can still rack up 200ms+ of INP. Wrap below-the-fold Alpine components in x-intersect so they initialize only when scrolled near.
<div x-data
x-intersect.once="$dispatch('boot-review-widget')"
class="min-h-[480px]">
<!-- review widget mounts on scroll-near -->
</div>The min-h-[480px] utility doubles as the CLS reservation (see section 3).
3. CLS: reserve space for everything that loads late
CLS failures on Magento come from four widgets in 95% of cases: the cookie consent banner, the hero slider's first slide, the Magewire-rendered review block, and promo bars injected via Page Builder. Every one of them fixes the same way — reserve the box with min-height or aspect-ratio before the content arrives.
Diagnostic
jq '.audits["cumulative-layout-shift"].details.items' lh-before.json[
{
"node": { "selector": "#cookie-consent" },
"score": 0.142
},
{
"node": { "selector": ".hero-slider" },
"score": 0.088
},
{
"node": { "selector": ".yotpo-reviews" },
"score": 0.051
}
]0.281 total CLS — well above the 0.1 "good" threshold — from three known offenders.
Fix the cookie banner
The banner injects DOM at the bottom after the consent script loads, pushing content up; the user dismisses it and content moves back down. Two layout shifts, one widget. Reserve the space with a placeholder.
<div id="cookie-consent" class="min-h-[88px] md:min-h-[64px]">
<!-- banner content injected here by consent script -->
</div>Animate max-height to 0 over 300ms on dismiss — user-initiated transitions do not count as layout shifts.
Fix the hero slider
Hero sliders on Page Builder or Hyvä CMS blocks render with no intrinsic dimensions. The first slide loads, the slider library re-measures, and everything below jumps. Set aspect-ratio on the slider container so the box is the correct height before any image arrives.
.hero-slider {
aspect-ratio: 16 / 9;
width: 100%;
}
@media (max-width: 768px) {
.hero-slider { aspect-ratio: 4 / 5; }
}Fix Magewire-rendered blocks
Magewire components show a server-rendered placeholder, then swap it for the hydrated version. An 80px placeholder hydrating to 320px is a 0.05 CLS hit. Match the placeholder height to the average final height.
<div wire:loading.class="opacity-50"
class="min-h-[320px]">
@include('magewire::reviews.list')
</div>4. Diagnosis tooling: Lighthouse CI + field data
Synthetic Lighthouse scores are a regression detector. Field data from real users is what Google ranks on. Run both — synthetic in CI on every deploy, field via web-vitals.js posted to a custom Magento controller.
Lighthouse CI with a regression budget
// lighthouserc.cjs
module.exports = {
ci: {
collect: {
url: ['https://staging.example.com/', 'https://staging.example.com/checkout'],
numberOfRuns: 3,
settings: { preset: 'mobile' }
},
assert: {
assertions: {
'categories:performance': ['error', { minScore: 0.85 }],
'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }]
}
}
}
};Field data via web-vitals.js
import { onLCP, onINP, onCLS } from 'https://unpkg.com/web-vitals@4?module';
function post(m) {
navigator.sendBeacon('/panth/vitals/collect', JSON.stringify({
name: m.name, value: m.value, rating: m.rating,
url: location.pathname
}));
}
onLCP(post); onINP(post); onCLS(post);The endpoint persists to a panth_web_vitals table partitioned by URL group. Grep the p75 weekly; flag any group above the "good" threshold.
Metric → typical Magento offender → fix
| Metric | Typical Magento offender | Fix |
|---|---|---|
| LCP > 2.5s | Hero image with loading="lazy", no fetchpriority | WebP <picture> matrix + preload + 10 KiB critical CSS |
| LCP > 2.5s on text | Web font with font-display: block | font-display: swap + preload .woff2 with crossorigin |
| INP > 200ms (Luma) | Knockout + RequireJS boot on checkout | Code-split, defer on requestIdleCallback |
| INP > 200ms (Hyvä) | Magewire-heavy below-fold blocks | Alpine x-intersect.once to defer init |
| CLS > 0.1 | Cookie banner injecting DOM late | min-height on the banner container |
| CLS > 0.1 | Hero slider with no intrinsic dimensions | aspect-ratio on the slider container |
The recipe checklist
- Run baseline Lighthouse mobile, save JSON. Identify the LCP element in DevTools Performance (CPU 4x, Network "Fast 4G").
- LCP image: ship the 1200w / 800w / 480w WebP matrix, preload,
fetchpriority="high", droploading="lazy". - LCP text:
font-display: swap, preload.woff2withcrossorigin. - Inline 10 KiB critical CSS, preload + async-load the rest.
- Luma INP: code-split checkout, defer non-critical RequireJS on
requestIdleCallback. Hyvä INP: wrap below-fold Alpine inx-intersect.once. - Reserve
min-heightoraspect-ratioon every late-loading container — cookie banner, hero slider, review block, promo bar. - Wire Lighthouse CI with a regression budget. Ship
web-vitals.jsfor field data. - Re-run baseline, compare, commit the diff.
Before and after on a real 2.4.9 + Hyvä store
Numbers below are from a UK fashion retailer running Magento 2.4.9 + Hyvä 1.3.5 on a 12-core VPS with Varnish + OpenSearch. 14 engineering hours across two weeks. CrUX 28-day field data confirmed the lab gains within ~3 weeks.
| Metric | Before | After |
|---|---|---|
| Lighthouse mobile performance | 52 | 88 |
| LCP (mobile lab) | 4.1s | 1.6s |
| INP (CrUX p75 field) | 412ms | 168ms |
| CLS (CrUX p75 field) | 0.28 | 0.04 |
| Total page weight (homepage) | 3.4 MB | 1.1 MB |
| JS execution time | 2.8s | 0.9s |
The merchant saw a 14% lift in mobile add-to-cart and an 8% lift in mobile checkout completion in the four weeks after deploy. Search Console flagged the URL group as "Good" on all three CWV within 28 days. None of the wins came from Varnish tuning.
What this is not
This recipe is not a substitute for server-side performance. If your TTFB is 1.4s because PHP-FPM is undersized or Varnish is misconfigured, no amount of critical CSS will save the LCP. Profile the server side first, then start on the client.
FAQ
Does this recipe work on Adobe Commerce Cloud?
Yes — every step is theme or layout level. The only Cloud-specific note is that cwebp runs in the build phase (hooks.build in .magento.app.yaml); the deploy phase has a read-only filesystem.
What if my LCP element is a Page Builder block?
Same fix, different access point. Edit the content-type template under view/adminhtml/pagebuilder/content_type/ and emit the <picture> markup from the storefront template.
Will minifying JS fix my INP score?
No. INP is dominated by main-thread execution time, not bundle size. A 100 KiB bundle that parses to 800ms still blocks for 800ms. Code-split first, minify second.
How often should we re-baseline?
Lighthouse CI on every PR as regression check. Full Lighthouse + CrUX snapshot quarterly — CrUX's 28-day rolling window makes that the right cadence.
Does Hyvä "just" solve Core Web Vitals?
Hyvä gets you from a Luma baseline of P=30–40 to roughly P=70–75. The last 15–20 points come from this recipe.
Is INP really replacing FID?
Yes — INP became a Core Web Vital in March 2024 and FID was retired. INP is harsher because it measures every interaction, not just the first.
Related reading
- Magento slow checkout — the 3 actual fixes
- Magento 2.4.9 upgrade traps and the 5 patches
- Hyvä theme development service
- Magento performance optimization service
Citations
- [1] web.dev — Interaction to Next Paint (INP). web.dev/articles/inp
- [2] web.dev — Largest Contentful Paint (LCP). web.dev/articles/lcp
- [3] web.dev — Cumulative Layout Shift (CLS). web.dev/articles/cls
- [4] Lighthouse v12 performance scoring. developer.chrome.com/docs/lighthouse
- [5] Chrome User Experience Report (CrUX). developer.chrome.com/docs/crux
I run fixed-scope Magento performance sprints on kishansavaliya.com with a full Lighthouse + CrUX baseline, the four-step LCP/INP/CLS recipe applied to your theme, Lighthouse CI wired in GitHub Actions, and a 30-day field-data dashboard. Fixed quote from $499 audit · $2,499 sprint · ~32h @ $25/hr. See hire me.