import React, { useState } from 'react';
// Definim loader-ul separat pentru a-l refolosi
const loadExportLib = () => import('./heavyReportGenerator');
export const ExportButton = () => {
const [isLoading, setIsLoading] = useState(false);
const handleExport = async () => {
setIsLoading(true);
const { generatePdf } = await loadExportLib();
await generatePdf();
setIsLoading(false);
};
return (
<button
onClick={handleExport}
onMouseEnter={loadExportLib} // Pre-descarcă în cache înainte de click
onFocus={loadExportLib}
disabled={isLoading}
>
{isLoading ? 'Se generează...' : 'Export Raport (PDF)'}
</button>
);
};Am văzut de prea multe ori echipe care cred că dacă sparg un bundle de 500KB în 30 de fișiere mici au rezolvat automat performanța. În teorie sună perfect: trimiți doar JS-ul de care userul are nevoie în secunda doi. În practică, dacă exagerezi, schimbi un download liniar pe un arbore de request-uri întârziate care blochează interfața.
La un proiect de analytics cu vreo 14k utilizatori zilnici, am prins un PR în care colegii puseseră React.lazy pe absolut orice: de la modalul de confirmare a ștergerii până la taburile secundare dintr-un profil. Pe hârtie, initial-bundle.js scăzuse de la 340KB la 190KB. Toată lumea aplauda în Lighthouse. În realitate, pe conexiuni mobile reale (4G instabil), utilizatorul dădea click pe un dropdown și aștepta 400ms cu ecranul înghețat până când browserul făcea roundtrip-ul pentru un chunk de fix 3.2KB.
Taxa invizibilă a fiecărui chunk
Fiecare fișier separat pe care îl generează bundler-ul tău (Webpack, Vite, Rollup) vine cu o taxă fixă. Nu vorbim doar de octeți, ci de modul în care browserul gestionează resursele.
În primul rând, ai runtime boilerplate. Bundler-ul trebuie să injecteze logica de module loading, mapping-ul ID-urilor de chunk și Promise wrappers. Pentru un chunk de 3KB de cod util, s-ar putea să trimiți 1KB doar overhead de împachetare.
În al doilea rând, ai latența de rețea. Chiar dacă ești pe HTTP/2 sau HTTP/3 și multiplexarea elimină limitarea celor 6 conexiuni TCP paralele, latența brută (RTT - Round Trip Time) nu dispare. Dacă chunk-ul B depinde de chunk-ul A, iar A a fost încărcat dinamic, ai creat un waterfall. Browserul descarcă A, îl parsează, execută codul, descoperă că are nevoie de B, inițiază un alt request și abia apoi randează.
Cum calculezi break-even-ul
Ca regulă practică testată în producție, eu aplic o formulă empirică simplă înainte să pun import() pe o componentă:
- Pragul de mărime (Size threshold): Dacă codul componentei (cu tot cu dependențe directe) are sub 20-25KB gzipped, nu îl sparg. Costul de runtime overhead și latency depășește milisecundele economisite la parse time-ul inițial.
- Probabilitatea de accesare (Hit rate): Dacă peste 60-70% dintre utilizatori ajung să deschidă componenta respectivă în primele 30 de secunde ale sesiunii (de exemplu, un meniu lateral pe mobil sau un pop-up de cookie/filtre), chunk-ul trebuie inclus direct în bundle-ul rutei curente.
- Volumul dependențelor grele: Aici e singurul loc unde break-even-ul e garantat pozitiv. Ai o librărie de export PDF (
pdfmake), un editor rich-text (tiptap) sau un motor de grafice (chart.js)? Vorbim de 80KB-200KB gzipped pe care doar 5% din useri le folosesc. Acolo dynamic import este obligatoriu.
Trade-off-ul real
Merge excelent pentru rute întregi (route-based splitting) și unelte grele folosite rar. E un dezastru pentru micro-componente de UI, meniuri sau taburi pe care userul le vrea instantanee la click.
Dacă vrei totuși să spargi componente medii fără să pedepsești experiența utilizatorului, soluția de compromis e prefetching-ul inteligent declanșat pe mouseover sau focus pe butonul care deschide modulul respectiv.