Încărcare: preconnect, prioritate pentru imaginea LCP, bundle, cache pe /uploads
Imaginea cea mai mare de pe pagina de start vine de pe alt domeniu, iar adresa ei nu apare nicăieri în HTML: browserul o află abia după ce descarcă și rulează pachetul JavaScript de intrare și mai face un drum până la API. Măsurat pe producție cu cache cald, cererea imaginii pornește la 1.065 ms de la începutul încărcării. Separat, pozele încărcate din panou primesc doar patru ore de cache, deși fiecare adresă e unică prin construcție și conținutul ei nu se schimbă niciodată. Pentru un vizitator de pe telefon asta înseamnă că prima imagine apare vizibil mai târziu decât ar trebui, iar la vizita următoare se descarcă din nou degeaba.
Ce am găsit
Nicio conexiune anticipată către api.hyperice.md, iar imaginea principală nu are prioritate
HTML-ul servit nu conține nicio legătură de conectare anticipată:
$ curl -s https://hyperice.md/ | grep -c 'preconnect'
0
Singurele legături din <head> sunt două preload de fonturi (SuisseIntl-Regular, SuisseIntl-Medium) și două modulepreload. Același rezultat pe /products/normatec-premier-hips.
Elementul cel mai mare al paginii vine însă de pe alt domeniu. În DOM-ul randat pe producție, document.querySelector('picture img').currentSrc este https://api.hyperice.md/uploads/75456663-5b36-455a-b8da-da024d7af431.jpg (158 KB), cu fetchPriority="auto", loading="auto" și fără width/height. Elementul e la frontend/src/components/HeroBanner.tsx:154:
<img src={image} alt="" aria-hidden className="absolute inset-0 h-full w-full object-cover" />
Lanțul de încărcare măsurat pe producție, cu cache cald — deci cifrele sunt minimul absolut:
- TTFB 101 ms
- pachetul de intrare cerut la 111 ms
-
domInteractive122 ms -
/api/bannerspornit la 789 ms, răspuns la 1.060 ms —frontend/src/components/HeroBanner.tsx:82 - cererea imaginii pornește la 1.065 ms
Adresa imaginii e cunoscută abia după ~1 s, pentru că se află doar după ce se execută 431 KB de JavaScript și se întoarce un apel API.
Efectul: browserul nu are cum să înceapă descărcarea mai devreme, și nici măcar negocierea TLS cu al doilea domeniu nu pornește în avans, pentru că nimeni nu i-a spus dinainte că va avea nevoie de el. Pe un telefon obișnuit, pe rețea mobilă, asta împinge ușor LCP-ul peste pragul de 2,5 s, chiar dacă imaginea în sine e mică.
Pachetul JavaScript de intrare are 431 KB și pornește înaintea oricărui alt lucru
$ curl -s -o /dev/null -w '%{size_download}' https://hyperice.md/assets/index-DuAUJSdq.js
431284
$ curl -s -o /dev/null -w '%{size_download}' -H 'Accept-Encoding: br,gzip' https://hyperice.md/assets/index-DuAUJSdq.js
133654
Cauza principală e identificată în cod: frontend/src/i18n.ts:4-6 importă static toate trei fișierele de traducere (en 43.709 + ro 82.686 + ru 68.488 = 194.883 octeți de JSON), iar frontend/src/i18n.ts:39-43 le înregistrează pe toate în resources. Pachetul descărcat conține simultan „Cumpără acum", „Купить сейчас" și „Add to cart" (22.059 caractere chirilice), plus șirul „Panou de administrare". Rutele publice sunt deja încărcate leneș (frontend/src/App.tsx:15-34), deci problema e strict în trunchiul comun: aproximativ două treimi din resursele de traducere ajung acolo fără să fie folosite.
Contează mai puțin ca trafic — 133 KB comprimat e rezonabil pentru intrarea unui SPA React — și mai mult ca timp de procesare: pe un telefon mediu, interpretarea și execuția a 431 KB de JavaScript țin firul principal ocupat, iar în tot acest timp nu se poate afișa nimic și nu se poate răspunde la atingeri.
Imaginile din /uploads au cache de patru ore, deși adresele lor sunt unice
$ curl -sI https://api.hyperice.md/uploads/b3856771-6b1b-43f4-86bf-376c2375b91a.jpg
cache-control: public, max-age=14400
cf-cache-status: REVALIDATED
content-length: 270123
Antetul acesta nu vine din aplicație. backend/src/app.ts:35 servește mapa cu app.use(UPLOADS_URL_PREFIX, express.static(UPLOADS_DIR)), fără opțiune maxAge, deci origin-ul trimite max-age=0; cele patru ore (14400 s) sunt implicitul Cloudflare pentru Browser Cache TTL.
Unicitatea adresei e garantată în cod: backend/src/lib/uploads.ts:27 scrie fiecare fișier ca `${randomUUID()}${ext}`, deci o poză nouă primește nume nou, iar o adresă existentă nu-și schimbă niciodată conținutul. Imaginea principală de pe pagina de start e una dintre acestea. Nu strică nimic azi — se pierde doar trafic și timp fără motiv.
De făcut
-
Adaugă <link rel="preconnect" href="https://api.hyperice.md" crossorigin>înfrontend/index.html -
Pune fetchpriority="high"pe imaginea dinfrontend/src/components/HeroBanner.tsx:154 -
Emite <link rel="preload" as="image">pentru imaginea primului banner dinbackend/src/services/render-page.ts, care scrie deja<head>-ul pentru fiecare pagină, ca adresa să fie cunoscută din HTML, nu după execuția JavaScript -
Trimite Cache-Control: public, max-age=31536000, immutablepe ruta care servește/uploadsdinbackend/src/app.ts:35 -
Verifică în panoul Cloudflare că Browser Cache TTL e pe „Respect existing headers", altfel antetul din aplicație rămâne suprascris
Criteriu de acceptare
- HTML-ul de la
https://hyperice.md/conținepreconnectcătreapi.hyperice.mdși unpreloadpentru imaginea primului banner. - În măsurătoarea de resurse, cererea imaginii din banner pornește înainte de răspunsul
/api/banners, nu după el. - Cel mai mare fișier din
frontend/dist/assetsare sub 300 KB necomprimat, iar pagina de start și pagina de produs se randează la fel. -
curl -sI https://api.hyperice.md/uploads/<orice>.jpgaratămax-age=31536000, immutable, iar o poză nouă încărcată din panou apare imediat, la adresa ei nouă.
Depinde de
Antetul de cache pentru /uploads nu poate fi dus până la capăt doar din repo: chiar cu immutable trimis de aplicație, valoarea rămâne suprascrisă dacă Browser Cache TTL din zona Cloudflare nu e setat pe „Respect existing headers". Setarea e la nivel de cont și cere accesul lui Vitalie în panoul Cloudflare.