// Exemplu de prefetch pentru a evita latența la interacțiunea utilizatorului
const loadHeavyChart = () => import(
/* webpackPrefetch: true */
/* webpackChunkName: "analytics-chart" */
'./components/HeavyChart'
);
// Componenta se încarcă în fundal după ce pagina principală e gata.
// Când utilizatorul dă click pe buton, codul e deja în cache.
button.addEventListener('click', async () => {
const { renderChart } = await loadHeavyChart();
renderChart();
});Am văzut prea des recomandarea oarbă de a pune import() dinamic peste tot pe unde prindem o componentă mai mare de 10 rânduri. Sfatul ăsta pornește dintr-o intenție bună – să micșorăm bundle-ul inițial –, dar aplicat fără cap, face mai mult rău decât bine.
Hai să vorbim pe cifre și pe ce am pățit eu la un proiect cu vreo 12.000 de utilizatori activi pe zi, unde ne-am trezit că aplicația se mișca mai prost pe telefoanele mobile deși scorul de Lighthouse pe desktop crescuse artificial.
Mitul „mai multe fișiere mici = mai bine”
Pe vremea HTTP/1.1, regula de aur era să pui totul într-un singur fișier mare ca să eviți limitarea de 6 conexiuni simultane per domeniu. Când a apărut HTTP/2 cu multiplexarea lui, mulți au crezut că gata, putem trimite 500 de fișiere mici de 1KB fără penalizări.
În realitate, lucrurile nu stau deloc așa. Fiecare fișier JS pe care îl separi prin dynamic import vine la pachet cu trei costuri ascunse:
- Overhead-ul de compresie (Brotli/Gzip): Algoritmii de compresie funcționează optim pe fișiere mari, pentru că pot identifica modele repetitive pe volume mai mari de text. Zece fișiere de 2KB comprimate separat vor ocupa cumulat aproape dublu față de un singur fișier de 20KB.
- Runtime-ul bundler-ului: Webpack sau Vite trebuie să injecteze un mic script de runtime care să gestioneze încărcarea asincronă, maparea chunk-urilor și tratarea erorilor de rețea.
- Latența de rețea (RTT): Pe o conexiune mobilă 4G cu semnal mediu, latența (Round Trip Time) poate fi ușor de 100-150ms. Chiar dacă HTTP/2 multiplexează, browserul tot trebuie să parseze primul chunk, să vadă ce alte chunk-uri mai cere (efectul de cascadă sau waterfall) și să facă cererile ulterioare.
Cum calculezi punctul de break-even?
Ca să nu mergi pe ghicite, există o matematică destul de simplă pentru a stabili dacă merită să spargi o componentă într-un chunk separat.
Regula mea de aur, testată în producție, este următoarea: nu separa niciodată o componentă dacă dimensiunea ei comprimată este sub 15-20KB, decât dacă este extrem de rar folosită (de exemplu, o pagină de setări avansate sau un panou de administrare).
Să facem un calcul rapid. Ai o componentă de chart-uri care are 30KB gzip.
- Varianta A (Inclusă în main bundle): Userul descarcă 30KB în plus la prima vizită. Pe o conexiune 4G medie, asta adaugă aproximativ 50-80ms la timpul inițial de descărcare. Pagina se randează complet din prima.
- Varianta B (Dynamic import): Pagina inițială se încarcă cu 80ms mai repede. Userul dă click pe tab-ul de statistici. Browserul face request pentru chunk-ul de 30KB. Până când requestul ajunge la server, serverul răspunde și browserul parsează JS-ul, trec cel puțin 250-300ms. În acest timp, userul vede un loader (sperăm!) sau, mai rău, un ecran înghețat.
Merge de minune să folosești dynamic import pentru rute mari sau librării masive (gen PDF generators, editoare de text bogat). Este însă groaznic să îl folosești pentru modale mici, dropdown-uri sau butoane interactive.
Cum eviți efectul de waterfall
Dacă totuși trebuie să folosești dynamic import pentru o componentă grea, dar vrei ca experiența să fie fluidă, folosește indiciile de preîncărcare (prefetch/preload). Astfel, browserul va descărca fișierul în fundal, cu o prioritate scăzută, imediat după ce s-a terminat încărcarea paginii principale.
Pe scurt: bundle splitting-ul nu este o soluție magică de tip „silver bullet”. Este un compromis direct între timpul de încărcare inițial și latența interacțiunilor ulterioare.
Voi cum abordați asta în proiectele voastre? Aveți o limită clară de KB de la care decideți să spargeți codul?