import React, { ComponentType, lazy } from 'react';
// Pattern simplu de lazy-loading cu capacitate de pre-încărcare la hover
export function lazyWithPreload<T extends ComponentType<any>>(
factory: () => Promise<{ default: T }>
) {
const Component = lazy(factory);
(Component as any).preload = factory;
return Component as typeof Component & { preload: () => void };
}
// Definim o componentă grea (ex: un editor sau un chart > 50KB)
const HeavyEditor = lazyWithPreload(() => import('./HeavyEditor'));
export const DashboardTab = () => {
return (
<div>
{/* Declanșăm download-ul când userul duce mouse-ul peste buton, economisind 200ms */}
<button
onMouseEnter={() => HeavyEditor.preload()}
onClick={() => /* schimba starea pentru a afisa editorul */ null}
>
Deschide Editor
</button>
</div>
);
};Am văzut prea multe aplicații unde devii pun React.lazy() sau import() pe fiecare modal și tab secundar, crezând că fac un serviciu utilizatorilor. Am făcut și eu greșeala asta acum vreo 3 ani la un dashboard B2B cu 12.000 de utilizatori zilnici. Rezultatul? Un Lighthouse score impecabil pe First Load și un UX absolut mizerabil imediat ce omul începea să navigheze.
Adevărul e că dynamic import-ul nu e gratuit. Vine la pachet cu un cost de rețea și parsing pe care mulți îl ignoră.
Unde am dat de zid cu mini-chunk-urile
În proiectul de care vă ziceam, am configurat bundler-ul să spargă totul ultra-granular. Am ajuns la peste 90 de fișiere .js generate în build output. Unele aveau sub 3KB gzipped.
Teoretic, încărcam doar ce trebuia. Practic, la fiecare accesare de pagină secundară, browserul făcea o cascadă (waterfall) de 4-5 request-uri HTTP pentru scripturi mici. Deși foloseam HTTP/2, latența de rețea pe conexiuni mobile sau Wi-Fi aglomerat adăuga 150-300ms întârziere la fiecare click. Userul vedea spinner-e peste tot pentru componente banale.
Overhead-ul ascuns pe care nu-l măsoară Lighthouse
Când faci bundle splitting agresiv, plătești trei taxe ascunse:
- Request RTT (Round Trip Time): Chiar dacă fișierul are 2KB, handshaking-ul, DNS-ul și latența fizică durează. 10 request-uri de 2KB se încarcă mult mai greu decât un singur request de 20KB.
- Execution Context & Parsing: Browserul trebuie să evalueze și să parsing-uiască fiecare module script separat. Trecerea prin Event Loop pentru fiecare chunk nou adaugă jank pe main thread.
- Cache Invalidation masivă: Dacă ai 100 de chunk-uri minuscule legate între ele prin hărți de dependențe, o schimbare într-un utilitar comun va invalida hash-ul la 30 de chunk-uri simultan. Utilizatorul le va descărca din nou pe toate.
Cum calculezi punctul de break-even
Trade-off-ul e simplu: mărimea economisită la bundle-ul inițial trebuie să fie mai mare decât costul de latență al request-ului separat.
După mai multe teste și măsurători cu Performance API în producție, am stabilit o regulă empirică pentru echipa noastră:
- Fișiere sub 20-30KB gzipped: Nu merită aproape niciodată chunk separat. Mai bine le lași în main bundle sau în chunk-ul rutei părinte.
- Librării grele (Chart.js, Leaflet, Monaco Editor, PDF-renderers): Aici break-even-ul e garantat. O librărie de 150KB scoasă din bundle-ul principal salvează masiv din TTI (Time to Interactive).
- Route-level splitting: Asta e singura regulă care trebuie aplicată aproape 100% din timp. Nivelul de rută e granița firească de încărcare.
Dacă chiar trebuie să spargi o componentă grea din interiorul unei pagini, folosește pattern-ul de preload on intent (la hover pe buton sau la focus), astfel încât request-ul să plece înainte ca userul să dea click.
Noi am redus timpul de încărcare perceput pe paginile interne cu 40% pur și simplu combinând 45 de mini-chunk-uri în 6 bundle-uri logice mai mari. Am pierdut 3 puncte în Lighthouse pe mobil, dar am câștigat o aplicație care se simte instantanee.
Voi ce praguri sau euristici folosiți când setați splitChunks în Webpack sau Vite? Mutați componente la nivel de UI în chunk-uri separate sau vă opriți doar la rute?