Shopify Product Page LCP Under 2 Seconds: A Core Web Vitals Playbook
Most Shopify product pages ship with an LCP between 3 and 5 seconds. Here's the engineering checklist we run through to drag that number under 2, without ripping out the theme or going headless.
Most Shopify stores we audit ship product pages with a Largest Contentful Paint somewhere between 3 and 5 seconds on a mid-tier Android over 4G. That's not a theme problem, and it's rarely a Shopify problem — it's a stack-of-decisions problem. This is the checklist we work through when a merchant asks us to get LCP under 2 seconds without going headless.
Why 2 seconds, and why LCP specifically
Google's "good" threshold for LCP is 2.5 seconds at the 75th percentile. We aim for 2.0 because the field data always lags the lab data by 15–30%, and because the real competitors on a product detail page aren't other Shopify stores — they're Amazon and whatever marketplace dominates your region. If your hero image paints in 1.8 seconds and theirs paints in 1.2, you lose the scroll.
LCP is also the one Core Web Vital that correlates most directly with revenue in our experience. CLS matters, INP matters, but a slow hero image is what makes a shopper bounce before they've even seen the price.
What actually counts as the LCP element on a PDP
On almost every Shopify product page we've measured, the LCP element is one of three things:
- The main product image (most common)
- The product title
h1(if the image lazy-loads incorrectly) - A hero banner above the product gallery (common on themes like Impulse or Prestige)
Before you optimize anything, open Chrome DevTools → Performance Insights → LCP, and confirm which element it is. We've watched teams spend a week optimizing images when the real LCP was a h1 blocked by a web font.
Step 1: Fix the hero image before anything else
Shopify's image_url filter and image_tag do most of the work for you, but defaults are wrong for a hero. Here's what we ship:
{%- assign hero = product.featured_media.preview_image -%}
<img
src="{{ hero | image_url: width: 800 }}"
srcset="
{{ hero | image_url: width: 400 }} 400w,
{{ hero | image_url: width: 600 }} 600w,
{{ hero | image_url: width: 800 }} 800w,
{{ hero | image_url: width: 1200 }} 1200w
"
sizes="(min-width: 750px) 50vw, 100vw"
width="{{ hero.width }}"
height="{{ hero.height }}"
alt="{{ hero.alt | escape }}"
fetchpriority="high"
loading="eager"
decoding="async">
Four things matter here:
fetchpriority="high"— tells the browser to pull this image before scripts, fonts, or lower-priority resources.loading="eager"— never lazy-load the LCP image. We see this mistake constantly because theme developers applyloading="lazy"globally.- Explicit
widthandheight— prevents CLS and lets the browser reserve space. - Tight
sizesattribute — most themes shipsizes="100vw", which forces the browser to download a 1200w image on desktop even though the slot is 600px wide.
Shopify's CDN serves WebP automatically when you use image_url, so you don't need to convert anything manually. Just stop uploading 4000px source images — resize to 2000px max before upload, because the CDN still has to transform the source and that eats TTFB on first request.
Step 2: Strangle the theme JavaScript
Dawn is reasonably fast. Most paid themes are not. The usual suspects:
- A full
jQuerybundle loaded in the head - Slick, Slideshow, or Flickity for the product gallery
- A "product recommendations" section that fetches above-the-fold
- Currency conversion scripts that block render
The one-line audit
In Chrome DevTools, open Coverage (Cmd+Shift+P → "Coverage"), reload the PDP, and sort by unused bytes. If any single JS file has more than 60% unused code and loads in the head, it's a candidate for defer or removal.
For the gallery specifically, modern CSS can do what Flickity used to do:
.product-gallery {
display: flex;
overflow-x: auto;
scroll-snap-type: x mandatory;
gap: 1rem;
}
.product-gallery > img {
scroll-snap-align: start;
flex: 0 0 100%;
}
We've shipped this on three stores in the last year and nobody noticed the "carousel" was gone — they just noticed the page felt faster.
Step 3: Audit your apps like they owe you money
This is where most of the LCP damage lives. Every Shopify app that injects a script tag via ScriptTag API or theme app extension adds between 20 and 300ms of blocking work. Open the Network tab, filter by JS, and sort by size. You'll typically find:
- A review app (Judge.me, Yotpo, Loox) loading 150–400KB above the fold
- An upsell app (ReConvert, Bold) with its own React bundle
- A "recently viewed" app that could be 20 lines of localStorage
- Two analytics apps doing the same thing
The ruthless rule
If an app injects more than 50KB of JavaScript and you can't point to revenue it generated in the last 90 days, uninstall it. Then — and this is the part teams forget — check the theme code for leftover script tags, because uninstalling an app does not always remove its snippet.
For review widgets specifically, almost all of them support lazy initialization. Yotpo has yotpo.initWidgets() you can call on scroll. Judge.me has a lazy-loading data attribute. Use them. The star rating in the product title can be rendered server-side from the app's API during a nightly sync into a metafield — no runtime script needed for the above-the-fold stars.
Step 4: Kill render-blocking fonts
Shopify themes love Google Fonts via @import, which is the slowest possible way to load a font. Switch to <link rel="preload"> with font-display: swap, or self-host via the assets folder:
<link rel="preload"
href="{{ 'Inter-Regular.woff2' | asset_url }}"
as="font"
type="font/woff2"
crossorigin>
Self-hosted fonts on Shopify's CDN are consistently 100–300ms faster than fonts.googleapis.com in our measurements, because you cut out a DNS lookup and a redirect.
Step 5: Handle third-party tags without destroying LCP
Facebook Pixel, TikTok Pixel, Klaviyo, GTM — these all want to load as early as possible for attribution accuracy. They also all tank LCP if you let them run on the main thread during initial paint.
The compromise we use:
- Load GTM with
asyncin the head (notdefer, GTM needs async) - Fire pixels on
requestIdleCallbackwith a 2-second fallback timeout - Use Shopify's Customer Events (web pixels) sandbox for anything that supports it — these run in a worker and don't touch your main thread
Shopify's web pixel sandbox is underused. If your pixel vendor supports it (most do now), moving them out of the theme into the sandbox typically shaves 200–500ms off Total Blocking Time.
Step 6: Measure the right way
Lighthouse in DevTools lies. It runs on your laptop, over your fibre connection, with no extensions loaded. Use these instead:
- PageSpeed Insights for field data (CrUX) — this is what Google actually ranks on
- WebPageTest with a Moto G Power profile on 4G for lab testing
- Shopify's Web Performance dashboard in the admin — it's CrUX data filtered to your store
We test every change against WebPageTest's Moto G Power + 4G profile because that's roughly the median device for most of our clients' traffic. If LCP is under 2 seconds there, it's under 2.5 seconds for 75% of real users.
Where we'd start
If you've got one afternoon, do this in order: confirm the LCP element in DevTools, fix the hero image attributes (fetchpriority, eager, correct sizes), and uninstall every app you can't justify. That alone has moved stores from a 4.2s LCP to a 2.1s LCP in a single sprint. Everything else — fonts, pixels, gallery rewrites — is cleanup after the big wins.
If you want a second pair of eyes on a specific store, our e-commerce engineering team does these audits as a fixed-scope engagement. But honestly, most of this you can ship yourself before the end of the week.
Want a team like ours?
72Technologies builds production software for the kind of teams who actually read this blog.
Start a projectKeep reading
Shopify App Bloat Is Killing Your Storefront: An Audit Framework That Works
Most Shopify stores we inherit are running 15+ apps, half of which inject scripts into every page load. Here's the audit framework we use to decide what stays, what gets replaced, and what gets ripped out.
Shopify Section Rendering API: The Underused Trick That Fixed Our Collection Page Jank
A field report on using Shopify's Section Rendering API to kill full-page reloads on filtered collection pages, with the gotchas we hit around caching, analytics, and infinite scroll.

WhatsApp Commerce Checkout in LATAM: What Actually Converts
A field report on building WhatsApp-first checkouts for stores in Mexico, Colombia and Brazil — what breaks, what converts, and the stack choices we'd repeat.
