Розробка галереї зображень товару з зумом для інтернет-магазину
Галерея зображень — основний інструмент вивчення товару. Покупець не може потрогнути продукт, тому якість та функціональність галереї напрямки впливають на довіру та конверсію. Технічно галерея — це більше, ніж набір зображень: це управління великими зображеннями, оптимізація завантаження, зум без втрати якості та бездоганне відчуття на тачскрині.
Формати та обробка зображень
Зберігаємо максимальну роздільну здатність (2000–4000px по довгій стороні), віддаємо потрібний розмір. Не подаємо 5MB оригіналу на кожну сторінку.
Pipeline обробки при завантаженні нового зображення:
Завантаження оригіналу → S3 (originals/, приватний)
↓ Worker (libvips)
Генерація варіантів:
- thumbnail: 100×100, JPEG 80%
- small: 400×400, JPEG 85%
- medium: 800×800, JPEG 90%
- large: 1600×1600, JPEG 95%
- webp: кожен варіант у WebP (30-50% менше)
↓
CDN (публічний бакет або Cloudflare Images)
libvips швидше ImageMagick в 4–8 разів. Для PHP — intervention/image з libvips-драйвером, для Node.js — sharp.
Сучасний <picture> з WebP:
<picture>
<source srcset="product-800.webp" type="image/webp">
<img src="product-800.jpg" alt="Nike Air Max 90 — вид сбоку"
width="800" height="800" loading="lazy">
</picture>
Структура компонента галереї
Типова розмітка карточки: основний слот (велике зображення) + смуга тумбнейлів знизу або зліва. На десктопе — горизонтальний або вертикальний strip, на мобайле — свайп за основним слотом.
function ProductGallery({ images, activeVariantImages }: Props) {
const [activeIndex, setActiveIndex] = useState(0);
const [isZoomed, setIsZoomed] = useState(false);
// При зміні варіанту — скид на перше зображення варіанту
useEffect(() => {
setActiveIndex(0);
}, [activeVariantImages]);
const allImages = [...activeVariantImages, ...images.filter(
img => !activeVariantImages.find(v => v.id === img.id)
)];
return (
<div className="gallery">
<MainSlot image={allImages[activeIndex]} onZoom={() => setIsZoomed(true)} />
<Thumbnails images={allImages} activeIndex={activeIndex} onSelect={setActiveIndex} />
{isZoomed && <LightboxOverlay images={allImages} startIndex={activeIndex} />}
</div>
);
}
Зум при наведенні (hover zoom)
Класичний паттерн для десктопа: при наведенні курсора на зображення, рядом (або поверх) з'являється збільшена область. Техніка реалізації:
function useHoverZoom(containerRef: RefObject<HTMLDivElement>, scale = 2.5) {
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleMouseMove = (e: MouseEvent) => {
const rect = containerRef.current!.getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width) * 100;
const y = ((e.clientY - rect.top) / rect.height) * 100;
setPosition({ x, y });
};
return { position, handlers: { onMouseMove: handleMouseMove } };
}
Зображення для зуму в 2–3 рази більше контейнера. Альтернатива: lens zoom — невеликий лінза на оригіналі показує область в збільшеному вікні рядом.
Pinch-to-zoom для мобайлу
react-zoom-pan-pinch — бібліотека для цього:
import { TransformWrapper, TransformComponent } from 'react-zoom-pan-pinch';
<TransformWrapper minScale={1} maxScale={4} doubleClick={{ mode: 'zoomIn' }}>
<TransformComponent>
<img src={largeImageUrl} alt={alt} />
</TransformComponent>
</TransformWrapper>
Коли зум активний горизонтальний свайп відключен, при scale=1 дозволен.
Lightbox / повноекранний перегляд
При клику на зображення (десктоп) або кнопку «Повний екран» — відкривається modal/lightbox з зображенням на весь екран. Вимоги:
- Закриття по Escape та клику за межами
- Переключення стрілками клавіатури (Left/Right)
- Swipe на мобайле
- Кнопки превью внизу
- URL не змінюється (або хеш:
#image-3)
yet-another-react-lightbox або PhotoSwipe (5kb gzip, відмінна мобільна підтримка).
Послідовна загрузка зображень
Неможливо грузити всі зображення галереї одразу. Стратегія:
-
Перше зображення —
loading="eager",fetchpriority="high", preload в<head> - Тумбнейли — маленькі файли, грузим все (вони невеликі)
-
Решта large зображень —
loading="lazy"або завантажуємо при клику на тумбнейл
function loadImageOnDemand(index: number, images: GalleryImage[]) {
if (loadedIndexes.has(index)) return;
const img = new Image();
img.src = images[index].largeUrl;
img.onload = () => setLoadedIndexes(prev => new Set([...prev, index]));
}
При переході до N — предзагружаємо N+1 та N-1.
Підтримка відео в галереї
Багато магазинів додають відео-огляд прямо в галереї — між зображеннями. Тумбнейл відео — стоп-кадр з іконкою play. При виборі — відео завантажується lazily (<video preload="none">).
Формати: MP4/H.264 (максимальна сумісність) + WebM/VP9 (менший розмір). Автопроигрывание — тільки muted.
<video controls preload="none" poster="poster.jpg">
<source src="review.webm" type="video/webm">
<source src="review.mp4" type="video/mp4">
</video>
Зображення по варіантам
Кожен варіант товару (колір) має свій набір зображень. При виборі варіанту:
- Галерея переключається на зображення цього варіанту
- Плавна анімація переходу (crossfade або slide)
- Тумбнейл вибраного варіанту підсвічується
У БД: product_images (id, product_id, variant_id, url, sort_order). При variant_id IS NULL — спільні зображення, показуються для всіх варіантів.
Терміни
- Базова галерея (слайдер + тумбнейли + lazy load): 3–5 робочих днів
- З hover-zoom та lightbox: 1–1.5 тижні
- З pinch-to-zoom, відео та по-варіантними зображеннями: 2–3 тижні
- Pipeline генерації превю (libvips + S3 + CDN): +1 тиждень







