Toate articolele
·PerformanțăLighthouseCore Web VitalsAccessibilitynext/image

De la 75 la 99: cum am vânat un LCP de 71 de secunde (și am dus Accessibility de la 85 la 96)

Performance 75 → 99 și Accessibility 85 → 96 pe un marketplace Next.js 16. LCP-ul de 71 de secunde, capcanele care m-au costat ore (build vechi rulat în paralel, contenția CPU) și de ce un singur `<img>` neoptimizat a făcut toată diferența.

De la 75 la 99: cum am vânat un LCP de 71 de secunde (și am dus Accessibility de la 85 la 96)

O sesiune reală de Lighthouse optimization pe un marketplace Next.js 16 — cu debugging adevărat, 3 capcane care m-au costat timp și de ce un singur <img> a făcut toată diferența.

Contextul

Rescriam un marketplace de anunțuri auto din CakePHP în Next.js 16 (App Router, MongoDB, imagini în public/). Funcțional mergea tot. SEO și Best Practices — 100. Dar Performance stătea sub 90 și Accessibility la 85, și nu voiam să livrez așa.

Obiectiv: 90+ la Performance și 95+ la Accessibility pe producție — nu doar „merge pe local".


Partea I — Performance: vânătoarea de LCP

Capcana #1: nu măsura niciodată performanța în dev

Prima greșeală pe care o face toată lumea: rulezi Lighthouse pe next dev și te sperii.

Dev mode în Next e intenționat neoptimizat: fără minificare, React în mod development (dublu-render, warning-uri), Turbopack overhead, fără cache headers. Performance pe dev e mereu 60–70, indiferent cât de bun e codul.

Regula: măsori pe un build de producție (next build && next start), cel mai apropiat proxy de remote.

pnpm build && PORT=3100 pnpm start
npx lighthouse http://localhost:3100/search/cars --only-categories=performance

Rezultat pe build de producție: Performance 75. Tot sub 90. Deci nu era doar dev — era o problemă reală.

Metrici contradictorii = miros de artefact

M-am uitat la breakdown:

MetricăValoareScor
First Contentful Paint1.0 s🟢 100
Largest Contentful Paint64.4 s🔴 0
Total Blocking Time90 ms🟢 99
Cumulative Layout Shift0🟢 100
Speed Index2.1 s🟢 99

Stai puțin. Speed Index 2.1s — pagina s-a umplut vizual în 2 secunde. Dar LCP zice 64 de secunde? Iar elementul LCP era un <h3> (text, nu imagine).

Când metricile se contrazic așa (FCP/SI excelente, LCP absurd), nu e cod „lent" — e ceva care blochează stabilizarea paginii. O resursă care se încarcă o veșnicie.

Vinovatul: <img> brut care servea PNG-uri de 2.5MB

curl -s http://localhost:3100/search/cars | grep -oc '/_next/image'
# 0

Zero /_next/image. Imaginile nu treceau prin optimizatorul Next. Se serveau direct:

<img src="/uploads/galleries/3317f641....png" />

Cât cântăreau originalele?

ls -la public/uploads/galleries/*.png   # ~2.5 MB / imagine

Cardul de anunț folosea <img> brut, nu next/image. Catalogul are ~12 carduri × 2.5MB = ~30MB de imagini neoptimizate. Sub throttling-ul CPU 4× al Lighthouse, încărcarea lor dura zeci de secunde → LCP exploda la 64s, în timp ce textul (FCP) apărea instant.

// înainte
<img src={listing.image} className="h-full w-full object-cover" />

// după
<Image src={listing.image} alt={listing.title} fill
       sizes="(max-width:768px) 100vw, 25vw" className="object-cover" />

Am convertit cardurile, galeria de pe pagina-mașină și hero-ul de pe homepage (alt PNG de 1.6MB). SVG-urile decorative le-am lăsat <img> — sunt vectoriale, next/image nu le optimizează oricum.

Capcana #2: măsuram build-ul VECHI tot timpul

Rebuild, restart, re-măsor:

PERFORMANCE: 71
LCP: 71.5 s
/_next/image: 0

Tot 71 de secunde. Și /_next/image tot 0. Sursa avea clar <Image>, dar HTML-ul randa în continuare <img>-ul vechi, cu className-ul vechi. Nu se lega.

lsof -ti:3100               # 72924 ← proces de dinainte de fix
tail -3 /tmp/prod.log
# port: 3100 is already in use
# Command failed with exit code 1

Iată. Serverul de la prima măsurătoare (înainte de fix) nu se oprise. pnpm start eșua silențios („port in use"), iar curl lovea în continuare build-ul vechi. Toate măsurătorile „post-fix" erau pe codul vechi.

Lecție: când restartezi un server pentru benchmark, verifică că ăla vechi chiar a murit. Un pnpm start care eșuează nu te oprește — te lasă să benchmark-uiești fantome ore în șir.

lsof -ti:3100 | xargs kill -9
pkill -9 -f "next-server"

Restart curat:

curl -s http://localhost:3100/search/cars | grep -oc '/_next/image'   # 132 ← ACUM da
<img loading="lazy" decoding="async" src="/_next/image?url=...&w=640&q=75" />

Rezultatul

MetricăÎnainte (<img> brut)După (next/image)
Performance7188
LCP71.5 s3.9 s
FCP0.9 s0.9 s
CLS00

Un singur fix — <img>next/image — a tăiat LCP-ul de la 71s la 3.9s.

Ultimul kilometru: LCP-ul era lazy

Imaginea LCP (primul card) era loading="lazy" — browserul o amâna intenționat. Soluția: priority pe imaginile above-the-fold ca să le preîncarce.

{listings.map((l, i) => <ListingCard key={l.id} listing={l} priority={i < 4} />)}

Capcana #3: scorul local dansează — e contenția CPU, nu codul

Scorul sărea între 70 și 88 pe același cod. Indiciul: TBT oscila între 60ms și 470ms. TBT (cât stă blocat main thread-ul) e pur CPU-bound — când oscilează pe cod identic, e mașina sufocată (dev + prod + build concurând, peste throttling-ul 4× Lighthouse), nu codul.

Și pe remote? 99.

Pe Vercel, motivele care trag local dispar: image optimization la edge (cache-uit, nu sharp on-demand la fiecare cerere), zero contenție CPU, CDN + HTTP/2 + Brotli. Predicția mea era 90+. Realitatea: Performance 99.


Partea II — Accessibility: 85 → 96

Lighthouse semnala 5 categorii. Patru au fost directe; una a cerut gândire.

Form labels, link names, heading order (ușoare)

Capcana de contrast: un singur roșu nu poate fi și bun pe buton și ca text

Aici a fost partea interesantă. Brand-ul folosea #d85151 pentru tot: fundal de buton (text alb pe el) și text (preț, link-uri) pe fundal închis.

Problema — niciunul nu trecea AA (4.5:1):

alb pe #d85151:          4.03   (buton — fail)
#d85151 pe card închis:  3.99   (text preț — fail)

Și nu le poți rezolva pe ambele cu o singură culoare: ca să treacă albul pe roșu ai nevoie de un roșu închis (#b83232); ca să treacă roșul ca text pe închis ai nevoie de un roșu deschis (#e06d6d). Sunt direcții opuse.

Soluția — token split:

--color-primary: #e06d6d;   /* TEXT pe dark — luminat, trece AA (5.0:1) */
--color-brand:   #b83232;   /* FUNDAL de buton — închis, alb trece AA (5.9:1) */

Apoi un sed chirurgical: cele 21 de bg-primary (butoane) → bg-brand, păstrând cele 52 de text-primary pe noul roșu deschis. Badge-ul „Sold" la fel — --color-danger închis la #c62828.

Touch targets (limitare cunoscută)

Singurul audit rămas: thumb-urile slider-ului dual-range se suprapun (două <input type="range"> cu inset:0 peste același track). Le-am mărit la 24px, dar Lighthouse vrea și spațiere între ele — imposibil când prin design ocupă același loc. Pentru 100 ar trebui reproiectat slider-ul; n-am considerat că merită ultimele puncte.

Rezultat: Accessibility 85 → 96.


Scorecard final

CategorieÎnainteDupă
Performance7599 (remote)
Accessibility8596
Best Practices100100
SEO100100

Ce iei cu tine

Un singur <img>next/image a făcut diferența între 71 de secunde și sub 4. Restul a fost să nu mă păcălesc singur pe drum.


Asta e exact tipul de muncă pe care îl facem în Audit & fix SEO / AI-SEO — audit real, fix-uri direct în cod. Vezi și optimizare SEO + AEO sau mentenanță continuă pentru monitoring Core Web Vitals.