Pozele încărcate din panou se reîncodează în WebP
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.