Core Web Vitals for Shopify Motion
Animated Shopify sections have a reputation for wrecking CWV scores. Most of that reputation is earned — but it's not motion itself that's the problem. It's motion implemented without thinking about the browser's critical path. Here's how Shopiflame sections stay green.
LCP — Largest Contentful Paint
LCP measures how quickly the largest visible element loads. For most sections this is either a hero image or a large text block.
Images: Hero images get loading="eager" and fetchpriority="high". Below-the-fold images get loading="lazy". Nothing lazy-loaded above the fold, nothing eagerly loaded below it.
GSAP timing: GSAP loads as type="module", which defers automatically. The browser paints content before GSAP runs — the section's static state is the first render. Animation is applied on top, not in place of, the initial layout.
{%- comment -%}
fetchpriority="high" only on the first, above-the-fold image.
Shopify's section.index tracks position on the page.
{%- endcomment -%}
{%- if section.index == 1 -%}
<img src="{{ image | image_url: width: 1600 }}"
fetchpriority="high"
loading="eager"
...>
{%- else -%}
<img src="{{ image | image_url: width: 1600 }}"
loading="lazy"
...>
{%- endif -%}
Self-hosted fonts: GSAP-animated headlines flicker when the font loads after the animation starts. We preload fonts as part of the section's {% stylesheet %} block — not via @font-face in a stylesheet that might arrive late:
<link rel="preload" href="{{ 'ClashDisplay-700.woff2' | asset_url }}"
as="font" type="font/woff2" crossorigin>
CLS — Cumulative Layout Shift
CLS is almost always caused by elements that don't declare their dimensions upfront, then resize when content loads.
Explicit dimensions: Every section declares aspect-ratio or explicit min-height on its container. The browser reserves the space before content arrives.
.sf-sticky-cards {
/* Reserve vertical space equal to the pin duration so nothing below shifts */
min-height: calc(var(--card-count) * 100vh);
}
No display: none on animated elements: GSAP's autoAlpha uses opacity: 0 and visibility: hidden — not display: none. Elements that start invisible still occupy space in the layout, so nothing shifts when they animate in.
Font size stability: We avoid clamp() values that change at the breakpoint where GSAP runs. If the heading jumps size at 768px and GSAP initializes at the same point, you get a layout shift + animation at once.
INP — Interaction to Next Paint
INP measures how quickly the page responds to user interactions. Long animation frames are the main cause of poor INP scores.
ScrollTrigger scrub: The scrub parameter (e.g. scrub: 1) smooths the timeline playback so it doesn't try to jump to a precise frame on every scroll event. This keeps the main thread clear.
requestAnimationFrame batching: GSAP batches all DOM writes to rAF automatically. Nothing in Shopiflame sections writes to the DOM outside of GSAP — no direct style assignments in scroll event handlers.
will-change: Applied only to elements that are actively being transformed, and removed after the animation completes. will-change: transform permanently on 8 card elements is a GPU memory leak.
gsap.set(cards, { willChange: 'transform, opacity' });
ScrollTrigger.create({
// ...
onLeave: () => gsap.set(cards, { willChange: 'auto' }),
onEnterBack: () => gsap.set(cards, { willChange: 'transform, opacity' }),
});
The reduced-motion short-circuit
When prefers-reduced-motion: reduce is active, none of the above matters — GSAP doesn't register any ScrollTriggers, no rAF budget is consumed, and the page is as fast as a static theme. This is also a useful testing mode: if your CWV scores are fine with reduced-motion but not without, the culprit is almost certainly in the animation setup, not the content.
Measuring
Use Google's PageSpeed Insights with a production URL (not a development store — Shopify throttles dev stores). Field data takes 28 days to populate; lab data is immediate. Both matter: lab tells you what changed, field tells you what real users experience.
For Shopify-specific measurement, the Speed tab in the Shopify admin shows a simplified Lighthouse score, but it only runs on the home page. Use PSI directly for section-level testing.
