Storefront performance¶
storefront-performance
Improve storefront Core Web Vitals (LCP, INP, CLS) using field data at the 75th percentile, image and font loading, caching of catalog pages, and lighter third-party scripts. Use when product or category pages feel slow, Web Vitals fail, or a theme, app, or script change needs a performance check.
Version 0.3.2 · MIT · Source · Rule
Install this skill
Copy the skill and its rule into your project:
The skill goes to .agents/skills/ and the rule to .agents/rules/. See Command line for other directories.
Judge speed by what real shoppers get, not one lab run. Core Web Vitals are measured at the 75th percentile of page loads, split by mobile and desktop. Thresholds and causes are in the Web Vitals notes.
| Metric | Good | Poor |
|---|---|---|
| LCP, Largest Contentful Paint | 2500 ms or less | over 4000 ms |
| INP, Interaction to Next Paint | 200 ms or less | over 500 ms |
| CLS, Cumulative Layout Shift | 0.1 or less | over 0.25 |
INP replaced First Input Delay as a Core Web Vital in March 2024.
Procedure¶
- Get field data per template (home, category, product, cart, checkout) from the Chrome UX Report, Search Console, or your own real-user monitoring with the
web-vitalslibrary. Lab tools explain causes but do not decide pass or fail. - Classify the p75 values:
node scripts/check-web-vitals.mjs vitals.jsonprints good, needs improvement, or poor for each template. - Fix the worst metric on the highest-traffic template first.
- LCP: the LCP element on a product page is usually the main product image. Serve it in a modern format at the displayed size, do not lazy-load it, give it
fetchpriority="high", and cut server response time with full-page caching for anonymous shoppers. - INP: break up long JavaScript tasks, defer third-party tags, and make variation pickers and add-to-cart update the UI before network work finishes.
- CLS: set
widthandheight(oraspect-ratio) on images, reserve space for banners, reviews widgets, and price blocks, and avoid inserting content above what the shopper is reading. - Recheck field data after 28 days, the window the Chrome UX Report uses.
Caching a store¶
Cache category and product pages for shoppers without a session. Bypass the cache for cart, checkout, account pages, and requests carrying cart or session cookies. Load the mini-cart count with a small request instead of disabling the page cache. Purge a product's page when its price or stock changes.
Third-party scripts¶
Chat widgets, review apps, A/B testing, and tag managers often cost more than the theme. List every script with its owner and purpose, remove unused ones, and load the rest after the page is interactive. A/B testing scripts that hide the page while loading hurt LCP directly.
Gotchas¶
loading="lazy"on the main product image delays LCP.- A cookie banner or promo bar inserted at the top shifts the whole page and adds CLS.
- Web fonts without
font-display: swapor a matching fallback can hide text or shift it. - Caching pages for logged-in shoppers can show one shopper's cart to another.
- A good Lighthouse score on a desktop connection says little about mobile p75 field data.
- Large carousels that load every slide image at once compete with the LCP image.
Verification¶
- Every template's p75 LCP, INP, and CLS is classified, mobile and desktop separately.
- The LCP image is not lazy-loaded and is requested early.
- Cart and checkout are never served from a shared page cache.
- After changes, field data at p75 improves over the next 28 days without a drop in conversion.
Evaluation¶
Worked examples and the checks they must satisfy are in evals/tasks.json.
References¶
Web Vitals notes¶
Thresholds¶
| Metric | Good | Needs improvement | Poor |
|---|---|---|---|
| LCP | at most 2500 ms | 2500 to 4000 ms | above 4000 ms |
| INP | at most 200 ms | 200 to 500 ms | above 500 ms |
| CLS | at most 0.1 | 0.1 to 0.25 | above 0.25 |
A page passes when all three are good at the 75th percentile.
LCP parts¶
LCP time splits into four parts. Find the biggest one first.
- Time to First Byte: server, cache misses, redirects.
- Resource load delay: the image is discovered late, for example set by JavaScript or CSS.
- Resource load duration: the file is too big.
- Element render delay: render-blocking CSS or scripts, or a hidden page from an A/B testing script.
Preload the LCP image when it is not in the initial HTML:
<link rel="preload" as="image" href="/img/mug-800.avif" imagesrcset="/img/mug-400.avif 400w, /img/mug-800.avif 800w" imagesizes="(max-width: 600px) 100vw, 50vw" fetchpriority="high">
Collecting field data¶
import { onLCP, onINP, onCLS } from "web-vitals";
function send(metric) {
navigator.sendBeacon("/vitals", JSON.stringify({
name: metric.name,
value: metric.value,
rating: metric.rating,
template: document.body.dataset.template,
}));
}
onLCP(send);
onINP(send);
onCLS(send);
Group the values by template and device, then take the 75th percentile.
Input file for the checker¶
Evaluation¶
Fixtures from evals/tasks.json. See evaluation fixtures for how the checks work.
slow-product-lcp¶
Mobile field data shows a p75 LCP of 4.6 seconds on product pages. The main image is lazy-loaded and a review widget loads at the top. What do you do?
Must include: 75th percentile, LCP, fetchpriority, 2500
Must not include: loading="lazy" on the main
Worked example
At the 75th percentile, 4.6 seconds is poor (above 4000 ms); the target is 2500 ms or less. The LCP element is the main product image, so remove lazy loading from it, add fetchpriority="high", and serve an AVIF or WebP at the displayed size with srcset.
Move the review widget below the product details, reserve its height so it does not shift the page, and load its script after the page is interactive. Check Time to First Byte too, and cache the product page for anonymous shoppers. Recheck the field data after 28 days.
cache-without-leaking-carts¶
Add full-page caching to a WooCommerce store without showing one shopper's cart to another.
Must include: cart, checkout, cookie, bypass
Must not include: cache every page for everyone
Worked example
Cache product, category, and home pages only for shoppers without a session, and bypass the cache for cart, checkout, and my-account pages, and for any request carrying the woocommerce_items_in_cart, woocommerce_cart_hash, or wp_woocommerce_session_ cookie, or a logged-in cookie.
Load the mini-cart count with a small uncached request after the page loads, so cached HTML never holds a shopper's cart. Purge a product's page when its price or stock changes.