- 8 min read
Core Web Vitals for E-commerce: A Practical Checklist
Core Web Vitals get discussed as an SEO ranking factor, which undersells them. On a store they are a revenue metric: the product page that paints in one second and responds to the first tap sells more than the one that does not, whatever Google does with the number.
Here is how the three metrics translate into things you actually change in a template.
LCP — the hero image, almost always
On a product page the Largest Contentful Paint element is the main image roughly every time. Three things decide it:
- It must not be lazy-loaded.
loading="lazy"on the LCP image is the most common self-inflicted wound in e-commerce templates. - It must be discoverable early. If it only appears after JavaScript runs, the browser cannot start the download in time.
- It must be the right size. A 2000px file displayed at 600px wastes the whole budget.
<link rel="preload" as="image"
href="/cdn/shop/products/hero_800x.jpg"
imagesrcset="/cdn/shop/products/hero_400x.jpg 400w,
/cdn/shop/products/hero_800x.jpg 800w,
/cdn/shop/products/hero_1200x.jpg 1200w"
imagesizes="(min-width: 1024px) 50vw, 100vw">
CLS — reserve the space
Layout shift on a store comes from a short list of culprits: images without dimensions, promo banners injected after load, review widgets, cookie bars, and fonts swapping to a metrically different fallback.
- Set
widthandheighton every image, or anaspect-ratioon its container. - Give injected elements a reserved slot in the markup, with the final height, rather than letting them push content down.
- Match the fallback font's metrics with
size-adjustso the swap does not reflow the paragraph.
@font-face {
font-family: "Brand Fallback";
src: local("Arial");
size-adjust: 104%;
ascent-override: 92%;
descent-override: 24%;
}
:root { --font-sans: "Brand", "Brand Fallback", system-ui, sans-serif; }
INP — stop blocking the main thread
Interaction to Next Paint measures the worst-feeling tap on the page, and it is nearly always third-party JavaScript. A cart drawer that takes 400ms to open is usually waiting behind an analytics script, not doing 400ms of its own work.
- Audit what each app actually loads. One abandoned trial app can own your INP score.
- Split long tasks and yield back to the browser between chunks.
- Do the visible work first; send the tracking call after the UI has updated.
async function yieldToMain() {
return new Promise((resolve) => setTimeout(resolve, 0));
}
async function handleAddToCart(variantId) {
openDrawerOptimistically(); // paint first
await yieldToMain();
await addToCart(variantId); // then the network
queueMicrotask(() => track("add_to_cart", { variantId })); // analytics last
}
The checklist I actually run
- Is the LCP image preloaded, correctly sized, and not lazy?
- Does every image and injected block reserve its space?
- Are fonts preloaded, limited in number, and metric-matched?
- Is theme JavaScript deferred, and is every third-party script still earning its place?
- Are you reading field data, not just a lab score on a cable connection?
Lab numbers tell you whether a change helped. Field data tells you whether you had a problem. You need both, in that order.