Product Image Optimization — Tại sao ảnh sản phẩm giết LCP của bạn
Ảnh sản phẩm chiếm 60-80% bandwidth của trang ecommerce. Một lỗi lazy load sai chỗ có thể phá hỏng LCP. Bài này đi qua các kỹ thuật tối ưu ảnh từ priority loading, LQIP, CDN transform đến gallery zoom on-demand.
Product Image Optimization — Tại sao ảnh sản phẩm giết LCP của bạn
LCP (Largest Contentful Paint) trên trang sản phẩm thường là ảnh hero hoặc ảnh đầu tiên trong grid. Lazy load sai chỗ, thiếu srcset, hoặc không dùng CDN transform — bất kỳ điều nào cũng có thể đẩy LCP lên 4-5 giây.
Vấn đề 1: LCP Trap — Lazy Load Ảnh Hero
tsx
// ❌ Sai: lazy load ảnh đầu tiên → LCP tệ
<img src={product.image} loading="lazy" alt={product.name} />
// ✅ Đúng: priority cho ảnh above the fold
// Với Next.js Image:
<Image
src={product.image}
alt={product.name}
width={600}
height={600}
priority // Preload, không lazy load
fetchPriority="high"
/>
// Với thẻ img thuần:
<img
src={product.image}
alt={product.name}
fetchpriority="high" // Hint browser tải sớm
// Không có loading="lazy"
/>Rule: Ảnh trong viewport ban đầu (trên fold) → không lazy load, dùng fetchPriority="high". Chỉ lazy load ảnh dưới fold.
Vấn đề 2: Responsive Images với srcset
tsx
// CDN transform URL pattern (Cloudinary example)
function buildImageUrl(publicId: string, width: number): string {
return `https://res.cloudinary.com/YOUR_CLOUD/image/upload/w_${width},f_auto,q_auto/${publicId}`;
}
function ProductImage({ publicId, alt }: { publicId: string; alt: string }) {
const widths = [320, 480, 640, 800, 1200];
const srcset = widths.map((w) => `${buildImageUrl(publicId, w)} ${w}w`).join(", ");
return (
<img
src={buildImageUrl(publicId, 640)} // Fallback
srcSet={srcset}
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
alt={alt}
width={640}
height={640}
style={{ aspectRatio: "1/1", objectFit: "cover" }}
/>
);
}Vấn đề 3: Blur Placeholder (LQIP) để Tránh Layout Shift
LQIP (Low Quality Image Placeholder) — tải ảnh base64 nhỏ xíu (< 1KB) làm blur placeholder trong khi ảnh thật đang tải.
tsx
// Cloudinary cho phép lấy LQIP ngay trong API response
// Hoặc generate lúc upload: w_20,e_blur:200
function ProductImageWithLQIP({
src,
lqip,
alt,
}: {
src: string;
lqip: string; // base64 của ảnh 20px blur
alt: string;
}) {
const [loaded, setLoaded] = useState(false);
return (
<div className="relative aspect-square overflow-hidden">
{/* LQIP blur placeholder */}
<img
src={lqip}
alt=""
aria-hidden
className="absolute inset-0 w-full h-full object-cover scale-110 blur-sm"
/>
{/* Ảnh thật */}
<img
src={src}
alt={alt}
className={`absolute inset-0 w-full h-full object-cover transition-opacity duration-300 ${
loaded ? "opacity-100" : "opacity-0"
}`}
onLoad={() => setLoaded(true)}
/>
</div>
);
}Vấn đề 4: Gallery Zoom — Tải Ảnh Full-size On-demand
tsx
function ProductGallery({ images }: { images: ProductImage[] }) {
const [activeIndex, setActiveIndex] = useState(0);
const [zoomSrc, setZoomSrc] = useState<string | null>(null);
const handleZoom = () => {
// Chỉ tải full-size khi user click zoom — không preload toàn bộ gallery
const fullSizeUrl = buildImageUrl(images[activeIndex].publicId, 2000);
setZoomSrc(fullSizeUrl);
};
return (
<>
<div className="relative">
<ProductImage
publicId={images[activeIndex].publicId}
alt={images[activeIndex].alt}
/>
<button onClick={handleZoom} className="absolute top-2 right-2 p-2 bg-white rounded">
🔍
</button>
</div>
{/* Thumbnail strip — lazy load thumbnails */}
<div className="flex gap-2 mt-2">
{images.map((img, i) => (
<button key={img.id} onClick={() => setActiveIndex(i)}>
<img
src={buildImageUrl(img.publicId, 80)}
alt={img.alt}
loading="lazy" // Thumbnails có thể lazy load
width={80}
height={80}
/>
</button>
))}
</div>
{/* Zoom modal — chỉ render khi cần */}
{zoomSrc && (
<ZoomModal src={zoomSrc} onClose={() => setZoomSrc(null)} />
)}
</>
);
}Vấn đề 5: CDN Auto Format và Compression
ts
// Cloudinary: f_auto chọn WebP/AVIF tự động theo browser support
// q_auto tối ưu quality tự động
const url = `https://res.cloudinary.com/cloud/image/upload/f_auto,q_auto,w_640/product.jpg`;
// Imgix tương tự:
const imgixUrl = `https://yoursite.imgix.net/product.jpg?w=640&auto=format,compress`;Key Takeaways
- LCP: ảnh hero và first product card không lazy load, dùng
fetchPriority="high" - srcset + sizes: cho browser tự chọn size phù hợp, tiết kiệm bandwidth đáng kể
- LQIP: tránh layout shift, tạo perceived performance tốt hơn
- CDN transform: Cloudinary/Imgix tự động resize, convert WebP/AVIF, compress
- Gallery zoom: tải full-size on-demand, không preload trước khi cần
If you found this helpful, leave a like!
Related Posts
Comments available after DB is connected.