fix(ecommerce): imagen principal del producto se ve borrosa en Chrome/Edge Android
Build and Push Frontend / Escaneo de Secretos (Gitleaks) (push) Successful in 1m2s
Build and Push Frontend / Construir y Subir Imagen (push) Successful in 8m58s

Reportado en vivo por el usuario: la foto principal (y el modal de
zoom interactivo) se ven borrosas en mobile, especificamente en
Chrome y Edge de Android.

Investigado a fondo antes de tocar nada: no es un archivo corrupto ni
un problema de nuestro cache/backend -- confirmado con
fetch()+createImageBitmap() (que evita <img>/srcset por completo) que
el archivo real decodifica perfecto a resolución completa (1200x1200).
El bug está en cómo Chromium elige el candidato del srcset con
sizes="100vw" + fill a DPR alto (2.6+, cualquier Android real):
currentSrc reporta la URL de un candidato grande, pero
naturalWidth/Height terminan reflejando un candidato mucho más chico
-- reproducido igual en Chromium headless, y consistente con lo
reportado en Chrome y Edge Android reales (mismo motor).

No hay forma de arreglar el algoritmo de selección de Chromium desde
acá. La foto principal y el modal de zoom pasan a unoptimized: al
quedar un solo recurso real por foto (sin candidatos de por medio),
no hay entre qué elegir mal. Costo: la foto pesa el archivo completo
en vez de una versión redimensionada -- aceptable para dos imágenes
puntuales donde la nitidez importa.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012Y4df3Z7odr9Cg8F2nw6uV
This commit is contained in:
devopsandClaude Sonnet 5 committed 2026-09-13 23:59:53 +00:00
1 parent 8d8546fd94
commit ccaddf546c
1 file changed
+14 -2
@@ -122,12 +122,24 @@ export function ProductGallery({ images, name }: { images: string[]; name: strin
onMouseMove={trackHoverZoom} onMouseMove={trackHoverZoom}
onMouseLeave={() => setHoverZoom(null)} onMouseLeave={() => setHoverZoom(null)}
> >
{/* unoptimized a proposito: confirmado en vivo (createImageBitmap
sobre los bytes reales vs. el <img> renderizado) que a DPR
alto (2.6+, cualquier Android real) Chromium reporta
naturalWidth/Height de un candidato del srcset mucho más
chico del que dice currentSrc -- bug del propio motor de
Chromium en la selección de candidato con sizes="100vw" en
fill, no algo corrupto en el archivo ni en nuestro caché
(el archivo decodifica perfecto vía fetch+createImageBitmap
fuera de <img>). Reproducido igual en Chrome y Edge Android
(ambos Chromium) y en Chromium headless. unoptimized deja
un solo recurso real por foto -- sin candidatos entre los
que el bug pueda elegir mal. */}
<Image <Image
src={selected} src={selected}
alt={name} alt={name}
fill fill
priority priority
sizes="(max-width: 1024px) 100vw, 50vw" unoptimized
className="object-cover" className="object-cover"
/> />
{/* Lupa: recuadro que marca la region ampliada -- solo cuando {/* Lupa: recuadro que marca la region ampliada -- solo cuando
@@ -162,7 +174,7 @@ export function ProductGallery({ images, name }: { images: string[]; name: strin
<div className="grid min-h-[70vh] grid-rows-[1fr_auto]"> <div className="grid min-h-[70vh] grid-rows-[1fr_auto]">
<div className="relative overflow-hidden bg-slate-950" onWheel={wheel} onPointerDown={pointerDown} onPointerMove={pointerMove} onPointerUp={pointerEnd} onPointerCancel={pointerEnd} onDoubleClick={() => applyScale(scale > 1 ? 1 : 2)} style={{ touchAction: "none", cursor: scale > 1 ? "grab" : "zoom-in" }}> <div className="relative overflow-hidden bg-slate-950" onWheel={wheel} onPointerDown={pointerDown} onPointerMove={pointerMove} onPointerUp={pointerEnd} onPointerCancel={pointerEnd} onDoubleClick={() => applyScale(scale > 1 ? 1 : 2)} style={{ touchAction: "none", cursor: scale > 1 ? "grab" : "zoom-in" }}>
<div className="absolute inset-6 transition-transform duration-100 will-change-transform" style={{ transform: `translate3d(${offset.x}px, ${offset.y}px, 0) scale(${scale})` }}> <div className="absolute inset-6 transition-transform duration-100 will-change-transform" style={{ transform: `translate3d(${offset.x}px, ${offset.y}px, 0) scale(${scale})` }}>
<Image src={selected} alt={name} fill sizes="95vw" className="select-none object-contain" draggable={false} /> <Image src={selected} alt={name} fill unoptimized className="select-none object-contain" draggable={false} />
</div> </div>
<div className="pointer-events-none absolute left-5 top-5 rounded-full bg-black/65 px-3 py-1.5 text-xs font-black text-white backdrop-blur">Rueda o botones para acercar · Arrastra para mover</div> <div className="pointer-events-none absolute left-5 top-5 rounded-full bg-black/65 px-3 py-1.5 text-xs font-black text-white backdrop-blur">Rueda o botones para acercar · Arrastra para mover</div>
</div> </div>