Journal

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.