Skip to content

Performance and Core Web Vitals

Hyprism is built to clear Shopify Theme Store performance gates with margin. This chapter covers the optimizations baked in, what targets to expect, and how to verify performance on your store.

Hyprism aims for these performance targets on every template:

MetricMobile targetDesktop targetTheme Store gate
Lighthouse Performance≥85 (median 92)≥95 (median 99–100)≥75 mobile
Largest Contentful Paint (LCP)<2.5s<1.5s<2.5s
Cumulative Layout Shift (CLS)<0.1<0.1<0.1
Total Blocking Time (TBT)<300ms<100ms<300ms
Interaction to Next Paint (INP)<200ms<200ms<200ms

The median after 4 iterative Lighthouse runs was:

  • Mobile: 92 (Performance), 96–100 (A11y), 100 (SEO) — only Cart shows SEO=69 because /cart is intentionally blocked from indexing via robots.txt.
  • Desktop: 99–100 (Performance), 96–100 (A11y), 100 (SEO).

⚠️ Read those numbers with their basis. They were measured on a development store carrying apparel demo content — a product page of roughly nine blocks. A technical store following recipe 22.2 puts twenty on the same page, most of them reading metafields. The blocks are server-rendered Liquid with no additional requests, so the cost is markup rather than JavaScript, but the figures above are not a promise for a page three times the size. Measure your own build — §14.5 says how.

The theme’s critical above-the-fold CSS (the styles needed before any section renders) is in assets/critical.css. It’s loaded synchronously in <head> with <link rel="preload"> so the browser fetches it early.

Non-critical CSS lives in {% stylesheet %} blocks inside individual section/block Liquid files — Shopify bundles these into per-section CSS files that load only when that section is on the page.

In layout/theme.liquid <head>:

{% raw %}{{ 'critical.css' | asset_url | stylesheet_tag: preload: true }}
{{ settings.type_body_font | font_url | preload_tag: as: 'font', type: 'font/woff2', crossorigin: 'anonymous', fetchpriority: 'high' }}
{{ settings.type_heading_font | font_url | preload_tag: as: 'font', type: 'font/woff2', crossorigin: 'anonymous', fetchpriority: 'high' }}
{% endraw %}```
All four font roles are preloaded, each at `fetchpriority: high`.
⛔⛔ **There are no preconnects at all, and that is deliberate.** Shopify serves theme assets *and* fonts from the store's own domain, so a preconnect to `cdn.shopify.com` or `fonts.shopifycdn.com` is an unused connection — Lighthouse flags it, and removing it freed roughly 1500 ms of critical-path latency on the home page. Only preconnect to an origin you genuinely fetch from cross-origin.
⭐ **Why the heading font is `high` and not `low`.** It pairs with `font-display: optional`, which only uses a webfont that arrives inside the browser's ~100 ms block window and otherwise keeps the fallback for that page view. That is what buys a shift-free first paint — but it also means a late font is a font the visitor never gets. The headline is the first thing measured for layout, so de-prioritising it worked against both goals at once.
### Image optimization
- **All images use `image_url` filter** for the Shopify CDN, which auto-converts to AVIF/WebP based on browser support.
- **Section-position-aware fetchpriority**: hero/first-section images get `fetchpriority="high"` for LCP; below-fold images get `loading="lazy"` for delayed loading.
- **`decoding="async"`** on every `<img>` — browser decodes images off the main thread, so image rendering doesn't block scroll responsiveness.
- **Subgrid card alignment** keeps all product cards in a grid identical-height (no layout shift as images load at different speeds).
### Passive event listeners
All scroll, touch, and wheel listeners use `{ passive: true }`:
```javascript
window.addEventListener('scroll', handler, { passive: true });
section.addEventListener('touchstart', handler, { passive: true });

This tells the browser “I won’t preventDefault” — the browser can start scrolling immediately without waiting for the JS to run. Massively improves INP.

Every theme JS script uses defer:

<script src="hyprism.js" defer></script>
<script src="hyprism-features.js" defer></script>

JS doesn’t block HTML parsing. It executes after DOMContentLoaded.

Instead of scroll listeners that fire on every scroll event, Hyprism uses IntersectionObservers for:

  • Sticky Product Bar (watches the main ATC)
  • Mobile Sticky Cart Bar (watches the in-flow summary)
  • Off-screen card visibility-hiding in slider carousels
  • Recently-viewed tracking (when a product page enters viewport, the product is added to localStorage)

IntersectionObservers fire only when an element crosses a threshold — orders of magnitude cheaper than scroll listeners.

  • Transform-only animations (translate, scale, rotate) — the browser composites these on GPU without main-thread cost
  • will-change only when transitioning — not set permanently (which would keep elements on a GPU layer always)
  • backdrop-filter only on explicit glass elements — not on overlay-wrappers (which would force the entire page through GPU compositor)

A few things that could be optimized but aren’t, because the trade-offs aren’t worth it:

Preload-link for product / article featured images (REVERTED)

Section titled “Preload-link for product / article featured images (REVERTED)”

Tried in v0.7-Round-2: <link rel="preload" as="image"> for the product page’s main image. On throttled-mobile (Lighthouse’s mobile profile = 4× CPU + Slow-4G), the preload competed with critical-path resources for bandwidth, and FCP regressed +0.6s. Net-negative — reverted.

Lesson: preload helps only for late-discovered resources (JS-driven images, dynamically-imported fonts). For in-the-HTML images, the browser’s default discovery is already optimal.

Tried in v0.7-Round-2: deprioritize the header logo to free bandwidth for the hero. On throttled-mobile, this delayed FCP because the logo is part of the first paint. Browser-default heuristics (auto-prio for above-fold images) were already correct. Reverted.

Lesson: don’t speculatively deprioritize above-fold resources — the browser knows better.

This score is constant across all 13 Hyprism templates because the failing items are Shopify-platform-level, not theme-fixable:

Failed ItemCauseFixable?
CSP script-src and object-src missingShopify edge sets Content-Security-Policy header that Hyprism can’t overrideNo — Shopify-level
HSTS max-age too lowShopify edge sets HSTS headerNo
No COOP (Cross-Origin-Opener-Policy)Shopify edgeNo
No Trusted TypesShopify edgeNo
Third-party cookies (shop.app)Shopify ecosystemNo

Dawn and Horizon score 77 too — reviewers are familiar with this plateau and don’t reject themes for it.

In Chrome / Edge:

  1. Open the page you want to test.
  2. DevTools (F12) → Lighthouse tab.
  3. Pick “Mobile” + “Performance, Accessibility, Best Practices, SEO” + “Slow 4G + 4× CPU”.
  4. Click “Analyze page load”.

Run 3–5 times and take the median — Lighthouse Mobile has natural variance (±5–15 points per run, even with no code changes).

Shopify Admin → Online Store → Themes → Customize → Performance tab. Shopify tests every published theme weekly and reports CWV.

Once your store is live with real traffic, Shopify’s Performance tab shows real-user CWV data (anonymized aggregate). This is more meaningful than Lighthouse because it reflects what real visitors experience.

Things merchants control that affect performance:

ChoiceImpactRecommendation
Hero video file sizeHighCompress to ≤2MB for autoplay-loops
Hero image sizeMediumUpload at 2× display size; let Shopify CDN serve optimized formats
Number of sections on home pageLowGenerally fine; Hyprism uses section-scoped CSS so unused sections aren’t loaded
Number of apps installedHighEach app injects JS. Apps often hurt performance more than themes.
Custom Liquid section contentVariableCustom Liquid runs server-side, so it’s free at runtime. JavaScript inside Custom Liquid can be costly.

INP is Google’s newer metric (replacing FID in 2024). It measures the longest input-to-paint delay during a page session — basically “how snappy does this page feel?”

Hyprism INP targets <200ms. To achieve this:

  • All JS listeners are passive where applicable
  • Main-thread tasks are <50ms each
  • Animations are GPU-composited (transform / opacity / filter)
  • IntersectionObserver replaces scroll listeners
  • AJAX cart updates are event-driven, no polling

If your INP is high on a specific page, check:

  1. Apps with heavy JS — Shopify Apps tab → Performance tab shows per-app impact
  2. Custom Liquid sections — large for loops in Liquid run server-side (fine) but JSON output rendered as <script> and parsed in JS can be slow if huge
  3. Marketing pixels — Meta Pixel, TikTok Pixel, etc. can be heavy

Chapter 15 — Localization — multi-language store setup.