Improve Largest Contentful Paint (LCP) by removing image transitions – Performance @ Shopify

No Comments
Improve largest contentful paint (lcp) by removing image transitions – performance @ shopify

AI Summary

A fade in transition on a Case-Mate product image delayed Largest Contentful Paint by six seconds. The image finished downloading just before 5 seconds, but a CSS rule tied to a reveal attribute held it at opacity: 0 until theme.js executed at around 12 seconds.

  • LCP is measured when the element paints, not when it downloads, so an invisible loaded image scores nothing.
  • The team had already removed lazy loading, added srcset and set fetchpriority="high". None of it helped, because the bottleneck was render delay.
  • Removing one HTML attribute moved the paint from after 12 seconds to after 7 seconds.
  • Keeping the transition is possible: extracted into its own async file loaded early, the penalty fell to about 0.5 seconds.
  • Diagnostic: in a WebPageTest waterfall, compare the LCP image download end against the LCP marker. A wide gap means something is hiding it.
Timeline diagram showing how a reveal attribute with opacity zero delayed a case-mate product image from painting until theme. Js ran at about 12 seconds, and how removing the transition moved largest contentful paint to about 7 seconds.
The product image finished downloading just before 5 seconds but did not paint until after 12 seconds, because a CSS rule tied to a reveal attribute kept it at opacity zero until theme.js executed.

Michael Gooding of Shopify's performance team published this case study in March 2023 after an office hours session with the team at Case-Mate. It is a short piece, and it documents one of the most under diagnosed classes of Largest Contentful Paint failure: the image is fine, the network is fine, and the page still fails Core Web Vitals because CSS is deliberately hiding the hero element until a late running script says otherwise.

The setup: an image that was already optimised

What makes the case study useful is that Case-Mate had done the obvious work first. Their product image had already been:

  • stripped of lazy loading, so it was eagerly requested;
  • made responsive with a srcset attribute so the browser could pick an appropriate size;
  • marked with fetchpriority="high" to raise it above other requests.

That is the standard hero image checklist, and it was all in place. The Liquid that produced it, simplified, looked like this:

{{ product
| image_url: width: 1000
| image_tag:
fetchpriority: "high",
reveal,
widths: "400,...,1000",
sizes: "(max-width: 999px) calc(100vw - 48px), 640px"
}}

Which rendered to markup along these lines:

     reveal
srcset="image.jpg?width=400 400w, ... image.jpg?width=1000 1000w"
sizes="(max-width: 999px) calc(100vw - 48px), 640px">

Everything in that tag is correct except one word: reveal.

Reading the waterfall: download time is not paint time

Using WebPageTest, the Shopify team compared when the image was downloaded against when it appeared. The product image, object 25 in that run, finished downloading just before 5 seconds. It did not appear on screen until after 12 seconds. Seven seconds of nothing, with the bytes already sitting in the browser.

MeasurementBefore (reveal attribute present)After (attribute removed)
Image download completesJust before 5 secondsJust before 5.5 seconds
Image visible in the browserAfter 12 secondsAfter 7 seconds
Gap between download and paintOver 7 secondsAbout 1.5 seconds
Reported LCP improvement6 seconds
If the transition is kept but loaded async and earlyImpact reduced to about 0.5 seconds

Figures reported by Michael Gooding for the Case-Mate product page, measured in WebPageTest. Object numbers differ between the two runs (25 before, 18 after) because removing the attribute changed the request order.

Chrome DevTools then supplied the cause: an opacity: 0 style was applied via the reveal attribute, making the image invisible from the moment it parsed. The JavaScript that faded it back into view lived in theme.js, and that file did not execute until around 12 seconds into the page load. The image was not slow. The image was hidden, and the thing holding the key was stuck in a queue.

The author's own summary of the size of the win is worth repeating: removing the attribute reveal recovered six seconds, which he describes as a full second of delay per letter of HTML.

Where this sits in the LCP breakdown

Google splits LCP into four consecutive sub parts. Knowing which one owns your time is the difference between a fix and a month of wasted image compression work.

LCP sub partWhat it coversTypical cause when it is the bottleneckFix that actually works
Time to First ByteServer response for the documentSlow origin, no cachingCache, edge rendering
Resource load delayGap from TTFB to the image request startingLate discovery, lazy loading, CSS background image, JS injected srcStatic markup, preload, remove loading lazy on the hero
Resource load durationThe image download itselfOversized or unoptimised file, slow CDNModern formats, srcset, sizes, compression
Element render delayGap from download finishing to the pixel appearingOpacity zero, animation, render blocking script, font swap, hydrationRemove the transition, unblock the script, this case study

Google's LCP breakdown. Case-Mate had already fixed the middle two parts. Their entire problem sat in the fourth row, which is the sub part most teams never look at.

Render delay is the sub part that hides in plain sight, because every popular diagnostic surfaces image weight and image priority while saying almost nothing about whether CSS is willing to draw the element.

The two fixes, and which one to pick

Option 1, remove the transition. The author's stated preference, and the one we would echo. In this case it meant deleting a single attribute from the image tag. Fade in animations on a hero image are almost pure cost: the user came to see the product, and animating it into existence delays the only thing they wanted.

Option 2, stop the transition code from render blocking. If the design team will not give up the effect, find the code that performs it and check when it runs. If it is buried in a bundle, extract it into its own file and load it asynchronously near the top of the document. On this same page that reduced the transition's impact to about 0.5 seconds, which is a defensible compromise.

There is a third approach the article does not cover but which is worth knowing: express the transition so that the element's default state is visible and JavaScript is only needed to enhance it. If the fade is driven purely by CSS with no JS gate, or if the hiding rule is scoped behind a class that JavaScript adds rather than removes, a late or failed script leaves you with a visible image instead of a blank box. Progressive enhancement, applied to animation.

How to check your own templates in ten minutes

1. Run the template in WebPageTest (mobile, 4G).
Open the waterfall, find the LCP element in the filmstrip.

2. Compare two numbers:
a. when the LCP image request finishes
b. the green LCP marker
A gap of more than about a second is a render delay bug.

3. Confirm the cause in Chrome DevTools:
Elements panel > select the LCP element > Computed
Look for: opacity, visibility, transform, clip-path,
animation-name, and any attribute selector rule.

4. Grep the theme for the coupling:
grep -rn "opacity: 0" assets/*.css
grep -rn "reveal\|animate\|scroll-trigger" assets/*.js

Do this on the template that earns the money, not the homepage. Product detail pages and category pages are where reveal animations get applied in bulk, and they are also where the conversion impact of a slow LCP is measurable.

Why this belongs in an SEO backlog, not just a developer one

LCP is a Core Web Vital with a good threshold of 2.5 seconds at the 75th percentile of real users. A page painting its hero at 12 seconds is not marginally failing, it is failing by a factor of five, and no amount of content work compensates. What makes this class of bug an SEO problem specifically is that it survives every checklist an SEO normally runs: the image is compressed, responsive, eagerly loaded and high priority. Every audit tool gives it a pass.

If you want to find these across a site rather than one URL at a time, start with our Core Web Vitals checker to identify which templates fail LCP in the field, then use the page speed analyzer to separate load delay from render delay. For the commercial framing, our Vodafone page speed case study puts a revenue number on exactly this kind of improvement, and our technical SEO audit includes render delay diagnosis on your key templates.

FAQ

Do CSS animations and fade in transitions hurt Largest Contentful Paint?

They can, badly. LCP is recorded when the element is actually painted to the screen, not when its bytes finish downloading. If a fade in starts the element at opacity zero and the script that removes that style runs late, LCP is pushed out to whenever that script runs. On the Case-Mate page this cost six full seconds.

Why is my LCP slow when the image downloads quickly?

This is the classic symptom of a render delay rather than a load delay. Open a WebPageTest waterfall, find the object row for the LCP image, and compare when the request completes against the LCP marker. A large gap points at CSS or JavaScript hiding the element, not at your CDN or image weight.

Does fetchpriority high fix a slow LCP image?

Only if the bottleneck is discovery or download priority. Case-Mate had already removed lazy loading, added srcset, and set fetchpriority high, and the image still painted at 12 seconds. Resource hints cannot help when the problem is a CSS rule keeping the element invisible after it has arrived.

How do I keep image transitions without wrecking LCP?

The source article gives a compromise: pull the transition code out of the main bundle into its own file and load it asynchronously near the top of the document. On the same page that cut the transition penalty from around six seconds to about 0.5 seconds. Removing the transition entirely remains the faster option.

What is the reveal attribute in a Shopify theme?

It is a theme level convention, not a web standard. Themes add a marker attribute such as reveal to elements they intend to animate, pair it with a CSS rule like [reveal] { opacity: 0 } and then have theme.js remove or override it. The attribute is harmless on its own; the coupling to a late running script is what causes damage.

Is Largest Contentful Paint still a Google ranking signal?

Yes. LCP is one of the three Core Web Vitals used in the page experience signals, with the good threshold at 2.5 seconds measured at the 75th percentile of real users. A page painting its hero image at 12 seconds fails that threshold by a very wide margin regardless of how well optimised the image file itself is.

Fast image, slow LCP?

We trace Largest Contentful Paint down to the sub part that owns the time and tell your developers exactly which rule or script to change.

Request an Advanced SEO Audit

Source: https://performance.shopify.com/blogs/blog/improve-largest-contentful-paint-lcp-by-removing-image-transitions

Claude Vincent is a technical SEO consultant focused on crawlability, rendering, and AI-search visibility. He writes the field guides and case studies at SEO ProCheck, with a bias toward the durable, unglamorous work that decides whether search engines and AI answer engines can actually read and cite a site.

About SEO ProCheck

Technical SEO consulting and GEO strategy with 20 years of enterprise experience. Case studies, resources, and tools for search and AI visibility.

Work With Me

Technical SEO audits, GEO strategy, site migrations, and international SEO. Hourly consulting for teams who need hands-on support, not just reports.

Subscribe to our newsletter!

More from our blog