.dev
Storefront templates

Performance

LCP and image rules for themes, the check:perf static scanner, accessibility budgets, and PageSpeed verification.

Target for every published theme: mobile lab Performance ≥ 80 and Accessibility / Best Practices / SEO at 100. The renderer already does the heavy lifting — custom component code is precompiled server-side so shoppers never run a transpiler — which means theme scores are decided almost entirely by images, fonts, contrast, and layout stability. All of that is yours.

Who owns what

ConcernOwner
Custom-code precompile (compiledCode), no client transpileLithium (the storefront)
Document TTFB / edge cachingHosting
Full Lighthouse CI gateStorefront CI
Hero/LCP images, thumbnails, fonts, contrast, tap targets, CLSYour theme

Image rules

Hero / LCP

  • Serve the hero at w=640–900 with srcSet / sizes — never a default w=1600 or w=1200 URL, and especially not one marked fetchPriority="high".
  • Exactly one fetchPriority="high" per page — usually the hero. Not the hero and the first product card.
  • The LCP candidate must be visible immediately (opacity: 1). No fade-in-on-load (opacity: 0 until onLoad) on the hero or the first row of cards.
  • Skip scroll-reveal animations on above-the-fold content — treat the first row (order <= 2) as already revealed.

Product thumbnails

Map every card image through a small cardImageUrl helper before paint, downsizing to roughly 320px: Unsplash w=320, Feeef storage ?width=320, Shopify CDN ?width=320. Never paint the full-size w=900 API URLs a list endpoint returns.

Seeded home products

The storefront may inject the home page's first product rows server-side via useSlotContext('homeProducts'). Use the seed and do not refetch-and-replace it — the swap causes layout shift and wastes the LCP work:

const seeded = useSlotContext('homeProducts');
const initial = Array.isArray(seeded) ? seeded : [];
const [products, setProducts] = React.useState(mapProducts(initial));
const [loading, setLoading] = React.useState(initial.length === 0);

React.useEffect(() => {
  if (initial.length > 0) return; // keep the paint stable — do not replace seeded rows
  // fetch only when the seed is empty…
}, [/* … */]);

Layout stability

Give every product/story/grid image explicit width / height attributes or a CSS aspect-ratio — unsized images are the classic CLS source.

Fonts

Use the template's props.font (e.g. "cairo") — the storefront loads it via next/font. Never inject a runtime Google Fonts stylesheet (fonts.googleapis.com) from theme code; you would double-load the font and add a render-blocking request.

Accessibility budget

These are the audits that most often cost themes their 100:

  • Contrast — body and muted text at 4.5:1 minimum (3:1 for large text). On light backgrounds use solid #555 / #666 for muted copy — not #888, not opacity: 0.7, not a 60% color-mix with transparency.
  • Tap targets — footer menus and legal links at 48×48 CSS px minimum; no tightly-stacked padding: 5px 0 link lists.
  • Text size — interactive labels at 12px or more (badges may be 12px, never 10px).
  • Link names — skip aria-label on RouterNav / links whose visible text already names the control (triggers the label-content-name-mismatch audit).
  • Decorative numerals — even aria-hidden display digits should hold 3:1 contrast; Lighthouse sometimes audits them anyway.
  • Account linkprefetch={false} on any /account link (or hide it) until that route exists; prefetching a missing route logs a 404 that costs Best Practices points.

The check:perf scanner

Every theme package scaffolded by the kit ships a static scanner:

npm run check:perf            # scan pages/ shared/ library/
npm run check:perf -- --strict  # warnings also fail

It greps your source (.tsx, .jsx, .ts, .js, .css) for the patterns above — no Lighthouse, no browser, instant feedback. What it flags:

Rule idLevelTrigger
hero-widtherrorUnsplash URL with w=1200+ and no mobile rewrite (heroSrc helper / w=640–900) in the same file
google-cairoerrorRuntime fonts.googleapis.com Cairo injection
contrast-hexerrorKnown-failing muted hexes (#888, #8DA0B5, #74777f)
opacity-onloadwarnopacity: 0 until onLoad fade-in patterns
account-prefetchwarn/account link without prefetch={false}
thumb-resizewarnProduct list paints photoUrl with no visible w=320 / cardImageUrl mapping

Errors exit non-zero; warnings pass unless --strict. The scan is heuristic — a clean run is necessary, not sufficient.

Lab Lighthouse

For a real score, run Lighthouse against a live demo store (requires Chrome; the script lives in the storefront workspace of the monorepo):

# from storefront/
npm run perf:lh -- https://your-store.feeef.store/
PERF_CATEGORIES=all npm run perf:lh -- https://your-store.feeef.store/

Or simply point PageSpeed Insights at your published demo store — same lab conditions, zero setup.

Workflow after edits

Run npm run check:perf in the theme package and fix any errors.

npm run build, then hard-refresh the draft preview (npm run dev).

After publishing, re-run lab Lighthouse (or PageSpeed Insights) on the demo store.

On this page