Разработка галереи изображений товара с зумом для интернет-магазина
Галерея изображений — основной инструмент изучения товара. Покупатель не может потрогать продукт, поэтому качество и функциональность галереи напрямую влияют на доверие и конверсию. Технически галерея — это больше, чем набор <img>. Это управление большими изображениями, оптимизация загрузки, зум без потери резкости и seamless опыт на тачскрине.
Форматы и подготовка изображений
Магазин должен хранить изображение в максимальном разрешении (2000–4000px по длинной стороне) и раздавать нужный размер под контекст. Раздавать оригинал 5MB на каждую страницу — грубая ошибка.
Pipeline обработки при загрузке нового изображения:
Загрузка оригинала → S3 (originals/, приватный)
↓ Worker (imagemagick / 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% меньше JPEG)
↓
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)}
isZoomed={isZoomed}
/>
<Thumbnails
images={allImages}
activeIndex={activeIndex}
onSelect={setActiveIndex}
/>
{isZoomed && (
<LightboxOverlay
images={allImages}
startIndex={activeIndex}
onClose={() => setIsZoomed(false)}
/>
)}
</div>
);
}
Зум при наведении (hover zoom)
Классический паттерн для десктопа: при наведении курсора на изображение, рядом (или поверх) появляется увеличенная область. Техника реализации:
function useHoverZoom(containerRef: RefObject<HTMLDivElement>, scale = 2.5) {
const [position, setPosition] = useState({ x: 0, y: 0 });
const [isHovering, setIsHovering] = useState(false);
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, isHovering, handlers: { onMouseMove: handleMouseMove, ... } };
}
Отображение зума через CSS transform: scale() с transform-origin в точке курсора. Изображение для зума должно быть в 2–3 раза больше контейнера — иначе зум будет размытым.
Альтернатива: lens zoom — небольшая линза на оригинальном изображении показывает область в увеличенном окне рядом. Реализуется через абсолютно позиционированный div с background-image и background-size: {scale*100}%.
Pinch-to-zoom для мобайла
На тачскрине hover-зум не работает. Нужен pinch-to-zoom и/или двойной тап для увеличения.
React-zoom-pan-pinch — библиотека для этого. Поддерживает pinch, double tap, wheel zoom, panning:
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 — только pan, при scale === 1 — разрешаем свайп.
Lightbox / полноэкранный просмотр
При клике на изображение (десктоп) или кнопку «Полный экран» — открывается modal/lightbox с изображением на весь экран. Требования:
- Закрытие по Escape и клику за пределами
- Переключение стрелками клавиатуры (Left/Right)
- Swipe на мобайле
- Кнопки превью внизу
- URL не изменяется (или хеш:
#image-3)
Для lightbox: yet-another-react-lightbox или PhotoSwipe (5kb gzip, отличная мобильная поддержка). PhotoSwipe работает нативно с touch events, имеет progressive loading (показывает thumb, пока грузится large).
Последовательная загрузка изображений
Нельзя грузить все изображения галереи сразу — это замедляет LCP. Стратегия:
-
Первое изображение —
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 (neighbour prefetch).
Поддержка видео в галерее
Многие магазины добавляют видео-обзор прямо в галерею — между изображениями. Тумбнейл видео — стоп-кадр с иконкой 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 неделя если с нуля







