Toate articolele
·de · Manilla Services·Studiu de cazPerformanțăCore Web VitalsNext.jsOptimizare

2 MB de conținut invizibil: payload-ul pe care o pagină îl trimitea fără să-l arate

Un homepage care trimitea la fiecare vizită textul complet a 462 de tipare — 2 MB de date pe care nu le afișa niciodată. Studiu de caz despre over-fetching în Next.js: cum cântărești payload-ul servit în loc să-l ghicești, de ce mobilul plătea 2 MB degeaba, și cum tai marfa invizibilă din pagină.

2 MB de conținut invizibil: payload-ul pe care o pagină îl trimitea fără să-l arate

E o capcană pe care instinctul n-o vede: un site poate fi construit curat, servit static, cu server rapid — și tot să pice testul de viteză pe mobil. Nu din cauza a ce se vede, ci din cauza a ce se trimite pe lângă.

The Cipher — o bibliotecă publică de tipare, cu 462 de intrări — avea un scor PageSpeed de 95 pe desktop și 68 pe mobil, cu un LCP de 5,9 secunde (roșu, „ai o problemă serioasă"). Aceeași pagină, același cod, aceeași bază de date. Diferența dintre 95 și 68 nu era codul. Era banda: pe simularea Google de telefon mediu pe 4G lent, ceva greu se descărca înainte ca pagina să poată respira.

Instinctul spune „mai comprimă pozele". Dar homepage-ul aproape că n-are poze — e un flux de text în mișcare peste un fundal întunecat. Deci frâna era în altă parte. Câteva ore mai târziu, mobilul era pe 98, desktopul pe 100, iar LCP-ul coborâse de la 5,9s la 2,1s. Articolul ăsta arată exact cum — și de ce mutarea decisivă n-a fost cea la care te-ai gândi.

Regula care schimbă totul: citești ce se servește, nu ce crezi că se servește

În loc să presupunem ce e greu, am cerut pur și simplu pagina așa cum o primește un vizitator și am cântărit-o. 2,2 MB. Pentru un homepage cu un titlu și un flux de text, cifra n-avea sens.

Am căutat în conținutul brut servit un singur cuvânt — numele câmpului care ține corpul editorial al fiecărui tipar. A apărut de 462 de ori.

Adică: homepage-ul trimitea, la fiecare vizită, textul complet al tuturor celor 462 de tipare — deși pagina nu afișează niciunul. Fluxul din față arată doar câte o frază scurtă; corpul întreg n-are ce căuta acolo. Era, la propriu, marfă invizibilă: doi megabaiți pe care browserul îi descărca, îi ținea în memorie, și nu-i desena niciodată.

De ce ajunseseră acolo? Dintr-un „ca să fie la îndemână": datele erau pasate componentei în întregime, în caz că vizitatorul deschide fereastra de detaliu a unui tipar. Numai că pe telefon deschiderea unui tipar te duce pe pagina lui (prerandată, instantanee) — fereastra de detaliu e doar pe desktop. Concluzia era usturătoare: mobilul descărca 2 MB pe care nu-i folosea niciodată.

Asta nu se ghicește. Se cântărește.

Ce am schimbat, mai exact

1. Pagina trimite doar ce afișează; restul vine la cerere

Am tăiat din payload tot ce homepage-ul nu randează la încărcare. Rămâne strict cât îi trebuie: titlul, fraza-cârlig și numărul fiecărui tipar — atât cât să funcționeze lista și căutarea rapidă. Corpul complet al unui tipar se aduce abia când e nevoie de el: la click pe desktop, fereastra de detaliu cere acel singur tipar, proaspăt, printr-o rută dedicată (~0,2s). Pe mobil, unde clickul navighează oricum la pagină, nu se mai aduce nimic în plus.

Rezultatul, măsurat pe pagina live: 2,2 MB → 793 KB. Aceeași pagină, aceeași experiență — doar fără marfa invizibilă.

2. Baza de date nu se mai citește de zeci de ori degeaba

Pe drum am dat peste o problemă care de fapt bloca și construcția site-ului: fiecare dintre cele 462 de pagini prerandate cerea, la build, toată colecția, de două-trei ori pe pagină. Pe o bază de date puțin mai lentă, build-ul intra în timeout de 60 de secunde pe pagină și nu se mai termina.

Fixul are două straturi: o memoizare la nivel de cerere (cele două-trei citiri identice devin una singură) și o variantă „ușoară" a interogării, care nu mai cară corpurile grele de text de care pagina de tipar oricum nu are nevoie. Build-ul a trecut din „nu se termină" în verde, cu toate cele 462 de pagini prerandate.

3. Pozele: o siglă de 1024 px pentru o insignă de 14 px

Singura imagine servită pe fiecare pagină era o siglă de credit — livrată la 1024×1024 pixeli și afișată la 14 pixeli. 96 KB pentru un punct cât unghia. Redimensionată la mărimea reală: 1,5 KB. Nicio diferență la ochi, de 60 de ori mai ușoară. Optimizarea de performanță bună e invizibilă pentru vizitator; contează doar pentru browser.

4. Fonturile: jumătate erau pentru litere care nu apar

Site-ul e integral în engleză, dar încărca, pentru fiecare font, și setul de caractere „latin extins" — diacritice și glife care nu apar nicăieri în text. Scos setul inutil → aproape jumătate din greutatea fonturilor a dispărut. În plus, ținta de browsere a fost mutată pe cele moderne, ceea ce a eliminat un strat de cod de compatibilitate pentru browsere vechi de care publicul ăsta n-are nevoie.

Misterul ecranului negru: un conflict de ceasuri, nu o eroare

A mai rămas un simptom pe care Lighthouse îl pedepsea dur: LCP render delay de ~2,4 secunde — pe filmstrip, o secundă bună de ecran negru la început. Site-ul are o intrare cinematică: un titlu-hero ține câteva secunde, apoi face fade în fluxul de text.

Cauza s-a dovedit elegantă și tăcută. Hero-ul dispărea pe un ceas CSS (un cronometru fix), iar fluxul din spatele lui apărea pe un ceas JavaScript (după ce codul se încarcă și rulează). Pe un telefon rapid, cele două se suprapun frumos. Pe simularea Google de telefon lent, JavaScript-ul întârzie câteva secunde — așa că hero-ul, condus de CSS, pleca înainte ca fluxul, condus de JS, să apuce să apară. Între ele: gol negru.

Fixul respectă exact intenția de design — hero care ține, apoi fade în flux — dar mută declanșarea: hero-ul rămâne vizibil până când JavaScript-ul confirmă că fluxul e gata, abia atunci se retrage. Pentru că JS-ul știe când e gata conținutul; CSS-ul, care merge pe cronometru, nu. Rezultatul: pe conexiune bună, aceleași 2-3 secunde de hero; pe conexiune lentă, hero-ul acoperă mai mult — dar niciodată ecran negru.

Ce confirmă cifrele, pas cu pas

Fiecare intervenție a fost re-măsurată pe site-ul live. Comparația dintre pași e partea utilă:

MomentScor mobilLCPCe s-a livrat
Start685,9spunct de plecare
Pasul 180~3,5spayload tăiat (2,2 MB → 793 KB) + pozele + baza de date
Pasul 2982,1secranul negru reparat + fonturi pe jumătate

Optimizarea de greutate — marfa invizibilă scoasă din pagină — a adus primul salt mare, de la 68 la 80. Iar sincronizarea hero-ului cu fluxul plus fonturile au dus restul drumului, până la 98, tăind LCP-ul sub pragul verde. Nici byte-urile singure, nici animația singură — ci amândouă, măsurate una câte una.

Scorul final:

Cum s-a lucrat, tehnic (pentru curioși)

Ce contează aici nu e câte linii s-au schimbat — sunt puține — ci ordinea mișcărilor: întâi diagnostic din ce se servește efectiv, abia apoi intervenție.

Ce a rămas uman: decizia „ce anume are nevoie pagina la încărcare vs. la cerere", judecata că o siglă decorativă suportă compresie agresivă, și fiecare „dă drumul în producție".

Întrebări frecvente

De ce era rapidă pe desktop și lentă pe mobil, cu același cod? Pentru că frâna era banda, nu procesarea. Desktopul, pe conexiune bună, înghite 2 MB fără să clipească. Telefonul, pe 4G lent, îi simte pe toți. Aceeași pagină, aceleași fișiere — doar că greutatea invizibilă lovește doar acolo unde banda e mică.

De ce nu a fost de-ajuns să comprimați imaginile? Pentru că homepage-ul aproape n-are imagini. Greutatea era date — textul complet a 462 de tipare trimis degeaba. Compresia de poze n-avea ce să atingă acolo. De-asta reflexul obișnuit n-ar fi mișcat scorul.

De unde știți care e adevărata problemă și nu ghiciți? Se cântărește pagina servită și se citește din ce e compusă greutatea, plus breakdown-ul metricii LCP (cât s-a pierdut la descărcare vs. la pictare). Măsurătoarea dă direcția; instinctul doar generează ipoteze de testat.

Fereastra de detaliu care se încarcă „la cerere" nu e mai lentă? Aduce un singur tipar în ~0,2 secunde și arată imediat titlul și cârligul, cât timp se completează restul — cu un indicator vizibil, nu ecran gol. Pe mobil nici nu se aplică: acolo navighezi la pagina tiparului, care e prerandată și se deschide instant. Compromisul e invizibil, iar câștigul — 2 MB scoși de pe fiecare vizitator — nu.

Ecranul negru era o problemă reală sau doar în Lighthouse? Rădăcina era reală (un conflict între un cronometru CSS și unul JavaScript), dar Lighthouse, care simulează un telefon foarte lent, o amplifica până devenea vizibilă. Reparând-o la rădăcină, dispare și în test, și pe device-urile lente reale.

Vezi și proiectul The Cipher — biblioteca publică de tipare din spatele acestui studiu de caz.

Din aceeași serie: De ce un site frumos pică testul de viteză — misterul NO_LCP — vânătoarea elementului LCP și cursa până la 91/100, pe hubul-frate.


Ai un site care „merge", dar pică la testul de viteză pe mobil? Cere o ofertă — îți spunem din ce e frânat, cu greutatea servită în față, nu presupusă. Vezi și serviciile noastre.