Core Web Vitals are a Google ranking signal and a direct measure of your store's user experience. Most Magento stores score 30–50 on mobile PageSpeed. Here's how to get to 90+.
Understanding what actually matters
Google measures three Core Web Vitals:
| Metric | What it measures | Good threshold |
|---|---|---|
| LCP (Largest Contentful Paint) | How fast the main content loads | < 2.5s |
| INP (Interaction to Next Paint) | How responsive the page is to clicks | < 200ms |
| CLS (Cumulative Layout Shift) | How stable the layout is | < 0.1 |
For most Magento stores, LCP is the biggest problem — slow server response time and large unoptimized hero images.
Fix 1: Time to First Byte (TTFB)
LCP starts with TTFB. If your server takes 800ms to respond, you've burned most of your LCP budget before the browser even starts rendering.
Target TTFB: < 200ms
The main levers:
- Full page cache (Redis-backed FPC)
- Server location (use a CDN edge node close to users)
- Reduced PHP execution time (module optimization, opcode cache)
# Check your current TTFB
curl -w "@curl-format.txt" -o /dev/null -s "https://your-store.com/"
# curl-format.txt: time_starttransfer: %{time_starttransfer}\n
Fix 2: LCP image optimization
The LCP element on most product and category pages is the hero image or first product image. Common mistakes:
No preload: The browser discovers the LCP image late because it's in CSS or lazy-loaded.
<!-- Add this in your <head> for the LCP image -->
<link rel="preload" as="image" href="/media/catalog/product/hero.webp" fetchpriority="high" />
Wrong format: JPEG/PNG instead of WebP or AVIF.
<!-- Use <picture> for modern format support -->
<picture>
<source srcset="hero.avif" type="image/avif" />
<source srcset="hero.webp" type="image/webp" />
<img src="hero.jpg" alt="Product" width="800" height="600" loading="eager" />
</picture>
Missing dimensions: Images without width and height attributes cause layout shifts (CLS) as they load.
Too large: A 3000px wide image served to a 400px mobile screen wastes bandwidth. Use srcset:
<img
src="product-400.webp"
srcset="product-400.webp 400w, product-800.webp 800w, product-1200.webp 1200w"
sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1200px"
width="800" height="600"
alt="Product name"
/>
Fix 3: Eliminate render-blocking resources
Every <script> and <link rel="stylesheet"> in <head> blocks rendering. Magento loads a lot of them by default.
Scripts: Use defer or async for non-critical JS.
<!-- Bad -->
<script src="bundle.js"></script>
<!-- Good -->
<script src="bundle.js" defer></script>
CSS: Inline critical CSS (above-the-fold styles) and load the rest asynchronously:
<style>/* critical CSS inlined here */</style>
<link rel="preload" href="styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
In Magento, you can configure this in requirejs-config.js and theme layout XML.
Fix 4: JavaScript bundle size
Magento ships with RequireJS and a large JS bundle. On a cold page load, the browser must download, parse, and execute all of it before the page is interactive.
Quick wins:
- Disable unused Magento JS modules in
requirejs-config.js - Enable JS bundling and minification:
bin/magento setup:static-content:deploy -f - Use Magento's built-in JS bundler or a custom Webpack/Vite build
# Enable JS/CSS merging and minification
bin/magento config:set dev/js/enable_js_bundling 1
bin/magento config:set dev/js/minify_files 1
bin/magento config:set dev/css/minify_files 1
Target: < 200KB of JavaScript on initial load (before user interaction).
Fix 5: CLS — prevent layout shifts
CLS > 0.1 is a ranking penalty. Common causes in Magento:
Images without dimensions: Always add width and height to <img> tags. This lets the browser reserve space before the image loads.
Web fonts causing FOUT: Use font-display: swap and preload your fonts:
<link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin />
Late-loading banners/widgets: If a header banner loads 500ms after the page, everything below it shifts down. Reserve space with min-height on the container.
Cookie consent banners: These are a major CLS source. Pre-allocate space or animate them in from outside the viewport.
Fix 6: Reduce third-party script impact
Analytics, chat widgets, review platforms, and ad tags are often responsible for 30–50% of a Magento store's JavaScript load.
Audit your third-party scripts:
- Open DevTools → Network → JS, filter by third-party domains
- Measure the size and load time of each
- Defer anything that doesn't need to run on initial load
For chat widgets and social proof tools: load them on first user interaction (scroll or mousemove event) instead of on page load.
Measuring and tracking progress
Use these tools:
- Google PageSpeed Insights — real-world CrUX data + lab data
- Web Vitals Chrome extension — see metrics as you browse
- Search Console — Core Web Vitals report (real user data, 28-day average)
- Lighthouse CI — automated testing in your deployment pipeline
Set a performance budget and fail the build if you regress:
# lighthouse-ci config
ci:
assert:
assertions:
first-contentful-paint: ['warn', {maxNumericValue: 2000}]
largest-contentful-paint: ['error', {maxNumericValue: 2500}]
cumulative-layout-shift: ['error', {maxNumericValue: 0.1}]
The 90+ checklist
- [ ] TTFB < 200ms (Redis FPC + CDN)
- [ ] LCP image preloaded with
fetchpriority="high" - [ ] Images in WebP/AVIF format with
width/heightattributes - [ ] No render-blocking scripts in
<head> - [ ] Critical CSS inlined
- [ ] JS bundle < 200KB initial
- [ ] Web fonts preloaded with
font-display: swap - [ ] CLS < 0.1 (reserved space for all dynamic elements)
- [ ] Third-party scripts deferred until interaction
Most stores can get from 40 to 80+ by fixing TTFB and LCP alone. Getting to 90+ requires addressing all the above.
Originally published on magevanta.com
Top comments (0)