Pagina nu desenează nimic până nu pornește JavaScript-ul: podeaua de 2,7 s a primei vopsiri
Magazinul nu desenează nimic până nu ajunge și pornește JavaScript-ul. Prima vopsire e la 2,7 s pe mobil, iar LCP nu poate fi niciodată mai devreme decât prima vopsire — deci pragul de 2,5 s cerut de checklistul de lansare este sub podeaua actuală. Nicio optimizare de imagini, de indicii sau de chunk-uri nu poate coborî sub ea; toate lucrează la timpul de deasupra.
Ce am găsit
Documentul servit nu are conținut
curl -s https://hyperice.md/ | wc -c
→ 8.509 octeți, din care capul are 7.575. Corpul are 871 de octeți — <div id="root"></div>, blocul de date al bannerului și <noscript>. Vizitatorul vede o pagină albă până când React randează.
Rendererul (backend/src/services/render-page.ts) o spune el însuși în comentariul de sus: „This is not server-side rendering: React still boots and draws the page exactly as it does today." Ce adaugă serverul azi e capul pe care îl citesc crawlerele, plus un <noscript> — invizibil pentru un browser normal.
Cifrele, măsurate pe producție
Lighthouse 13.4.1, mobil, mediana a 3 rulări:
| Pagină | FCP | LCP |
|---|---|---|
| Prima pagină | 2,72 s | 4,44 s |
/collections/shop-all |
2,83 s | 3,55 s |
/products/hypervolt-3-pro |
2,69 s | 4,22 s |
De unde vin cele 2,7 s
Ce trebuie să ajungă înainte ca vreun pixel să fie desenat, citit din raportul de rețea:
| Fișier | Transferat |
|---|---|
| documentul | 4,6 KB |
index-*.css (blochează randarea) |
14,4 KB |
rolldown-runtime-*.js |
0,9 KB |
jsx-runtime-*.js |
15,9 KB |
common-*.js |
25,1 KB |
index-*.js (pachetul de intrare) |
87,5 KB |
| subtotal | 148,4 KB |
| două fonturi, preîncărcate cu prioritate | 44,8 KB |
poza hero, preîncărcată cu fetchpriority="high" (doar prima pagină) |
155 KB |
Aproximativ 350 KB se bat pe aceeași conexiune simulată de 1,6 Mbps, adică ~200 KB/s: 1,75 s doar de transfer, plus stabilirea conexiunii. Asta e podeaua.
Ce ar rămâne dacă zona de sus s-ar vopsi din HTML
Documentul plus fișierul de stiluri fac 19 KB. Prima vopsire ar veni la ~0,9-1,2 s, iar LCP-ul ar deveni momentul în care ajunge poza principală — care e deja preîncărcată din cap și, după #29 (closed), ar cântări 74 KB în loc de 155. Estimare: LCP 1,8-2,2 s, adică sub prag.
Cum se poate face aici
Nu prin randare React pe server: cele două aplicații se livrează ca imagini separate, iar backendul nici nu are dist/-ul frontendului — îl citește peste rețea. Dar rendererul construiește deja documentul și cunoaște deja conținutul de sus: heroHead() citește bannerele publicate ca să numească prima poză în cap, iar bannerData() le scrie în document ca date. Îi mai lipsește doar să scrie și markup-ul.
Verificat empiric pe React 19.2.7, versiunea din proiect: createRoot peste markup pus de server în #root îl înlocuiește curat la prima randare, cu zero mesaje în consolă. Deci nu e nevoie de hidratare și nici de un element separat care se scoate după.
De făcut
-
Rendererul scrie în #rootmarkup-ul zonei de sus: pentru prima pagină, primul slide al hero-ului; pentru pagina de produs, poza pe care o anunță deja preload-ul. -
Markup-ul și clasele să fie identice cu ce randează React, ca înlocuirea să nu miște nimic. CLS se măsoară înainte și după — azi e 0,011 pe prima pagină și 0,045 pe produs, și trebuie să rămână sub 0,1. -
Un test care ține markup-ul serverului și componenta React în pas, ca să nu divergă în tăcere. -
Măsurare pe producție după deploy: mediana a 5 rulări, nu una.
Criteriu de acceptare
Pe producție, mobil, mediana a 5 rulări Lighthouse: FCP sub 1,5 s și LCP sub 2,5 s pe prima pagină și pe pagina de produs, cu CLS rămas sub 0,1.
Desprins din #10 (closed), care rămâne deschis până când cifrele confirmă. Se combină cu #29 (closed) (poza hero e un upload neoptimizat, 155 KB în loc de 74).