Skip to content

Pozele încărcate din panou se reîncodează în WebP

Vitalie requested to merge 29_optimize-uploads into main

Prima jumătate din #29 (closed): reîncodarea la încărcare. A doua, conversia fișierelor existente, vine separat pentru că atinge .gitlab-ci.yml și acel MR nu primește pipeline.

De ce contează

Elementul pe care se măsoară LCP-ul pe prima pagină este un astfel de fișier: uploads/75456663-….jpg, 780×1400, image/jpeg, 158.584 de octeți, fără nicio prelucrare. Trecut prin codul din acest MR: 79.318 octeți WebP, −50,0 %. Varianta de desktop: 270.123 → 167.768, −37,9 %. Cifre măsurate cu webpReplacementFor pe fișierele reale descărcate de pe producție, nu estimate.

Azi sunt în regulă doar din întâmplare — cine le-a pus le-a pregătit înainte. Un admin care încarcă mâine o poză de 6 MB direct din telefon o pune pe prima pagină și nimic nu-l oprește.

Decizia de arhitectură: un singur fișier, nu o pereche

Issue-ul cerea un frate .webp lângă original și ca Picture să-l ofere și pentru /uploads/. Nu așa s-a făcut, și e o decizie, nu o omisiune. Picture.tsx explică de ce /uploads/ e exclus: un <picture> nu dă a doua șansă — dacă browserul alege un <source> care dă 404, vede o imagine ruptă, nu originalul. Un frate ar fi garantat doar pentru încărcările noi, deci contractul ar fi fost mincinos exact pentru fișierele vechi.

Un singur fișier scoate problema din discuție: adresa din baza de date arată direct spre WebP, Picture.tsx rămâne neatins, iar preload-ul din renderer numește automat fișierul corect. Cele două bife din issue rămân nefăcute prin decizie.

Politica

Plafon 2048 px pe latura lungă, nu 1600 ca în tools/image-optimizer: acolo e corect, pentru că nimic din frontend/public nu se afișează mai lat de 960 CSS px — dar hero-ul e full-bleed, deci lățimea lui este viewportul, iar bannerul de desktop are exact 2048 px. Copiat orbește, plafonul ar fi resamplat chiar imaginea pe care se măsoară LCP-ul.

Buget 300 KB, calitatea cedează înaintea pixelilor, withoutEnlargement. .rotate() aplică orientarea EXIF — fără ea, o poză făcută pe telefon culcat rămâne culcată definitiv, pentru că reîncodarea aruncă EXIF-ul. GIF-urile nu se ating (pot fi animate; sharp ar citi doar primul cadru). AVIF nu se folosește: un al doilea format readuce problema fratelui.

Reîncodarea nu poate strica o încărcare: orice eroare e prinsă, originalul rămâne, ruta răspunde 201. O poză mai grea e mai bună decât un panou care refuză să salveze.

Dependență nouă

sharp în dependencies (nu dev — stagiul de runtime din Dockerfile face npm ci --omit=dev). E standardul pentru asta, repo-ul o folosește deja în tools/image-optimizer, și nu există altă cale de a reîncoda în Node.

Verificat de mine, pe ramura rebazată pe main

Comandă Rezultat
backend/ npm run build exit 0
backend/ npm test 534 de teste în 35 de fișiere, toate trec
frontend/ npx tsc --noEmit -p tsconfig.app.json exit 0
frontend/ npx tsc --noEmit -p tsconfig.test.json exit 0
frontend/ npm run build exit 0
frontend/ npm test 226 de teste în 29 de fișiere, toate trec
conversia celor două bannere reale 158.584 → 79.318 și 270.123 → 167.768

Nu închide #29 (closed) — se închide odată cu MR-ul de seed, care duce conversia la fișierele deja existente pe producție.

Merge request reports