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
| Concern | Owner |
|---|---|
Custom-code precompile (compiledCode), no client transpile | Lithium (the storefront) |
| Document TTFB / edge caching | Hosting |
| Full Lighthouse CI gate | Storefront CI |
| Hero/LCP images, thumbnails, fonts, contrast, tap targets, CLS | Your theme |
Image rules
Hero / LCP
- Serve the hero at
w=640–900withsrcSet/sizes— never a defaultw=1600orw=1200URL, and especially not one markedfetchPriority="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: 0untilonLoad) 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/#666for muted copy — not#888, notopacity: 0.7, not a 60%color-mixwith transparency. - Tap targets — footer menus and legal links at 48×48 CSS px minimum; no
tightly-stacked
padding: 5px 0link lists. - Text size — interactive labels at 12px or more (badges may be 12px, never 10px).
- Link names — skip
aria-labelonRouterNav/ links whose visible text already names the control (triggers the label-content-name-mismatch audit). - Decorative numerals — even
aria-hiddendisplay digits should hold 3:1 contrast; Lighthouse sometimes audits them anyway. - Account link —
prefetch={false}on any/accountlink (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 failIt greps your source (.tsx, .jsx, .ts, .js, .css) for the patterns above — no
Lighthouse, no browser, instant feedback. What it flags:
| Rule id | Level | Trigger |
|---|---|---|
hero-width | error | Unsplash URL with w=1200+ and no mobile rewrite (heroSrc helper / w=640–900) in the same file |
google-cairo | error | Runtime fonts.googleapis.com Cairo injection |
contrast-hex | error | Known-failing muted hexes (#888, #8DA0B5, #74777f) |
opacity-onload | warn | opacity: 0 until onLoad fade-in patterns |
account-prefetch | warn | /account link without prefetch={false} |
thumb-resize | warn | Product 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.
Related
Marketing pixels
How Meta and TikTok events fire from themes — automatic events, the FeeefPixels helper, dynamic Lead/Purchase objectives, and testing.
Authoring conventions
The design-system.md contract, currency symbols, GFM product bodies, the Feeef footer credit, and naming rules every theme must follow.