Core Web Vitals for Shopify: a practical fix list
LCP, INP and CLS explained in Shopify terms, with the specific theme and app changes that move each metric — ordered by how much they actually help.
Most Core Web Vitals advice for Shopify is either “install a speed app” or a wall of theory. This is the middle version: what each metric means for a storefront, and the specific changes that move it, ordered by impact.
Before you start, get real numbers. The Shopify speed score in your admin is a lab test on a synthetic device — useful for spotting regressions, useless for knowing what your customers experience. Use the Chrome UX Report data in PageSpeed Insights, which reflects real visits to your store over the previous 28 days.
LCP — Largest Contentful Paint
What it measures: how long until the biggest thing above the fold has painted. On almost every Shopify homepage this is the hero image. On a product page it is usually the main product photo.
Target: under 2.5 seconds for 75% of visits.
Fixes, in order of payoff
1. Stop lazy-loading your hero. This is the most common own-goal in Shopify theming. loading="lazy" on the LCP image tells the browser to deprioritise the one thing the metric measures. Your hero should be loading="eager" with fetchpriority="high". Everything below the fold should be lazy.
2. Serve an image sized for the device. A 2400px hero delivered to a 390px phone is roughly 30× more pixels than needed. Shopify’s image_url filter with a proper srcset and a correct sizes attribute solves this:
{% assign img = section.settings.hero_image %}
<img
src="{{ img | image_url: width: 1200 }}"
srcset="{{ img | image_url: width: 480 }} 480w,
{{ img | image_url: width: 800 }} 800w,
{{ img | image_url: width: 1200 }} 1200w,
{{ img | image_url: width: 1800 }} 1800w"
sizes="(min-width: 1024px) 100vw, 100vw"
width="{{ img.width }}"
height="{{ img.height }}"
loading="eager"
fetchpriority="high"
alt="{{ img.alt | escape }}">
The sizes attribute is the part people get wrong. If it does not describe the real rendered width, the browser picks the wrong candidate and all the work above is wasted.
3. Preload the hero. One line in theme.liquid, inside the {% if template == 'index' %} branch so you are not preloading a homepage image on every page.
4. Audit your app scripts. Open DevTools → Network, filter to JS, sort by size. Anything over 50KB that is not your theme deserves a question. Apps you trialled and abandoned frequently leave script tags behind — check Online Store → Themes → Edit code for orphaned snippets.
5. Self-host fonts or use font-display: swap. A render-blocking font request from a third-party origin adds a connection setup you cannot control. If the hero text is the LCP element, this is directly on the critical path.
INP — Interaction to Next Paint
What it measures: how long the page takes to visibly respond after a tap or click. It replaced First Input Delay because FID only measured the first interaction and only measured the delay, not the response.
Target: under 200ms for 75% of visits.
This is the metric Shopify stores fail most often, and it is almost always apps. Every app that adds a global click listener, every chat widget that boots on load, every review widget that re-renders the page — they all compete for the same main thread that needs to handle the tap.
Fixes
1. Count your apps honestly. Open your storefront and list every third-party feature on the page. For each one, ask whether it earns its place. The median store we audit is running two apps it forgot it installed.
2. Defer everything non-critical. Chat widgets, review carousels, popups and analytics should not load during the initial render. Most modern apps support an idle-load or on-interaction mode; a surprising number just do not have it turned on.
3. Break up long tasks. If you have custom JavaScript doing work over a large collection, chunk it. Anything holding the main thread for more than 50ms is a long task and directly hurts INP.
4. Watch for layout thrash in sticky headers. Reading getBoundingClientRect() in a scroll handler and then writing a style forces synchronous layout on every frame. Batch reads and writes, or use IntersectionObserver instead.
CLS — Cumulative Layout Shift
What it measures: how much visible content jumps around while the page loads.
Target: under 0.1.
CLS is the easiest of the three to fix because the causes are finite:
- Images without dimensions. Always set
widthandheight(or a CSSaspect-ratio). The browser then reserves the box before the file arrives. - Web fonts swapping. A fallback font with different metrics reflows the text when the real font lands. Use
size-adjustandascent-overridein your@font-faceto match metrics, or a tool that generates them. - Injected banners. Announcement bars, cookie notices and free-shipping bars that insert themselves at the top of the DOM after paint push everything down. Reserve their height in CSS or render them server-side.
- Embedded content. Reviews, Instagram feeds and video embeds that expand after loading. Give them a min-height.
What to do this week
If you only have an afternoon:
- Run PageSpeed Insights on your homepage, a collection page and your best-selling product page. Record the field data numbers.
- Fix
loadingandfetchpriorityon your hero image. - Add
widthandheightto every image in your theme that lacks them. - Uninstall one app you are not using, and check the theme code for what it left behind.
- Re-measure in 28 days, because field data is a rolling window and will not move overnight.
That list has, in our experience, moved more stores from red to green than any speed-optimisation app ever has.
If you want to know what a small app looks like, all six of ours publish their bundle size on their pages. Ours are under 30KB. Most are not.