skills/infrastructure-performance/image-optimization-cdn/SKILL.md
Speed up your store by automatically resizing and converting product images to WebP/AVIF, adding lazy loading, and serving via CDN
npx skillsauth add finsilabs/awesome-ecommerce-skills image-optimization-cdnInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Product images are typically the largest assets on e-commerce pages and the single biggest contributor to poor Largest Contentful Paint (LCP) scores. An optimized image pipeline resizes images to the required dimensions, converts to modern formats (WebP, AVIF) for 30–50% smaller file sizes, delivers from a CDN close to the user, and applies lazy loading to images below the fold.
| Platform | Built-In Image CDN | What You Need to Do |
|----------|-------------------|-------------------|
| Shopify | Shopify CDN (Fastly) — automatic WebP conversion, responsive sizing via URL params | Use Liquid img_url filter with size params; ensure all <img> tags have width and height attributes; add loading="lazy" to below-fold images |
| WooCommerce | None by default | Install Imagify or ShortPixel plugin for automatic WebP conversion; add Cloudflare as CDN (free tier) for global delivery |
| BigCommerce | BigCommerce CDN — automatic WebP conversion, responsive sizing | BigCommerce serves images via CDN automatically; optimize by specifying image dimensions in the URL and setting proper <img> attributes in templates |
| Custom / Headless | None — you build it | Use Cloudinary (managed) or Sharp (self-hosted) with a CDN; see implementation below |
Shopify CDN automatically handles WebP conversion and resizing. Your job is to use it correctly in Liquid templates:
image_url filter with explicit dimensions:<!-- Good: Shopify CDN serves WebP at the right size -->
<img
src="{{ product.featured_image | image_url: width: 800 }}"
srcset="{{ product.featured_image | image_url: width: 400 }} 400w,
{{ product.featured_image | image_url: width: 800 }} 800w,
{{ product.featured_image | image_url: width: 1200 }} 1200w"
sizes="(max-width: 640px) 100vw, 50vw"
width="800" height="800"
loading="lazy"
alt="{{ product.featured_image.alt | escape }}"
/>
loading="eager" only for the hero (LCP) image:{% if forloop.first %}
{%- assign loading = 'eager' -%}
{%- assign fetchpriority = 'high' -%}
{% else %}
{%- assign loading = 'lazy' -%}
{%- assign fetchpriority = 'auto' -%}
{% endif %}
<img loading="{{ loading }}" fetchpriority="{{ fetchpriority }}" ... />
fetchpriority="high" or is too largeWooCommerce serves images from your server without CDN or WebP conversion by default. Two steps are needed:
Step 1: Add WebP conversion (pick one):
Step 2: Add CDN delivery:
Step 3: Configure lazy loading (WordPress 5.5+ handles this automatically):
WordPress adds loading="lazy" to all <img> tags automatically since version 5.5. Verify it's working:
loading="lazy" attributeloading="eager" — configure this in your theme's templateStep 4: Set correct image dimensions in WooCommerce:
wp media regenerate --only-missingBigCommerce serves all images via its CDN with automatic WebP conversion for supported browsers. Optimize your theme templates:
getImageSrcset Handlebars helper to generate responsive srcset attributes:<img
src="{{getImage image 'product_size'}}"
srcset="{{getImageSrcset image 1x='product_size' 2x='zoom_size'}}"
loading="lazy"
width="800" height="800"
alt="{{image.alt}}"
/>
Option A: Cloudinary (managed — recommended for most stores)
// lib/cloudinary.js
import { v2 as cloudinary } from 'cloudinary';
cloudinary.config({ cloud_name: process.env.CLOUDINARY_CLOUD_NAME, api_key: ..., api_secret: ... });
export function getProductImageUrl(publicId, { width, height }) {
return cloudinary.url(publicId, {
transformation: [{
width, height, crop: 'fill', gravity: 'auto',
quality: 'auto:good',
fetch_format: 'auto', // Auto-serves WebP/AVIF based on Accept header
}],
secure: true,
});
}
srcset in your product image component:export function ProductImage({ publicId, alt, priority = false }) {
const widths = [240, 400, 600, 800, 1200];
const srcSet = widths.map(w => `${getProductImageUrl(publicId, { width: w, height: w })} ${w}w`).join(', ');
return (
<img
src={getProductImageUrl(publicId, { width: 400, height: 400 })}
srcSet={srcSet}
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 25vw"
alt={alt}
width={400} height={400}
loading={priority ? 'eager' : 'lazy'}
fetchPriority={priority ? 'high' : 'auto'}
/>
);
}
Option B: Sharp (self-hosted image processing)
Use Sharp for a self-hosted pipeline. Process images at upload time and store variants in object storage (S3, R2, Backblaze):
// lib/image-processor.js
import sharp from 'sharp';
const SIZES = {
thumbnail: { width: 240, height: 240 },
card: { width: 400, height: 400 },
detail: { width: 800, height: 800 },
zoom: { width: 1600, height: 1600 },
};
export async function processAndStoreProductImage(inputBuffer, productId) {
const urls = {};
for (const [sizeName, dims] of Object.entries(SIZES)) {
const webpBuffer = await sharp(inputBuffer)
.rotate() // auto-rotate from EXIF
.resize({ ...dims, fit: 'cover', withoutEnlargement: true })
.webp({ quality: 80 })
.toBuffer();
const key = `products/${productId}/${sizeName}.webp`;
await uploadToStorage(key, webpBuffer, 'image/webp');
urls[sizeName] = `https://images.yourstore.com/${key}`;
}
return urls;
}
Serve with immutable CDN headers (include a content hash in the filename for cache busting):
// Image URL response headers
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
width and height on all <img> tags — this prevents Cumulative Layout Shift (CLS) by reserving space before the image loadsloading="eager" and fetchpriority="high" only on the LCP image — applying eager to all images defeats lazy loading and increases initial page weightwithoutEnlargement: true in Sharp or Cloudinary's width/height params| Problem | Solution |
|---------|----------|
| LCP image not the one you expected | Use Chrome DevTools → Performance tab to identify the actual LCP element; it may be a background image or a hero banner, not the product image |
| AVIF encoding too slow for on-demand transforms | Pre-generate AVIF at upload time; use WebP for real-time transforms (50× faster than AVIF encoding) |
| Sharp native binaries missing in production | Add sharp to dependencies (not devDependencies); for Docker builds match the target architecture: npm install --platform=linux --arch=x64 sharp |
| Images not loading from CDN on first request | Pre-warm your top product images by fetching their CDN URLs after upload; don't rely on first visitor to warm the cache |
| WooCommerce images not converting to WebP | Verify your host supports the GD or Imagick PHP extension (both required by Imagify/ShortPixel); contact your host if not available |
tools
Let shoppers save products to a wishlist, share it with friends, and get notified when saved items come back in stock or drop in price
development
Build a themeable storefront with design tokens and CSS custom properties that supports white-labeling, multi-brand variants, and dark mode
development
Speed up product discovery with instant search suggestions, fuzzy typo matching, and category-aware results powered by Algolia or Elasticsearch
development
Build a mobile-first storefront with thumb-friendly navigation, sticky add-to-cart buttons, and touch-optimized components for high mobile conversion