Kodėl jūsų React aplikacija kraunasi kaip senas traktorius
Turbūt visi esame matę tą liūdną vaizdą – atidari React aplikaciją, o ji kraunasi ir kraunasi… Vartotojas žiūri į baltą ekraną arba sukantį loader’į, o jūs jau įsivaizduojate, kaip jis nervingai spaudžia F5 arba, dar blogiau, uždaro naršyklės langą. Problema dažniausiai slypi ne jūsų kode (na, gal tik iš dalies), o tame, kaip šis kodas pateikiamas naršyklei.
Kai kuriate React aplikaciją be jokių optimizacijų, webpack’as ar kitas bundler’is suvynioja viską į vieną didžiulį JavaScript failą. Visus komponentus, visas bibliotekas, visą logiką – viską į vieną bundle. Rezultatas? Vartotojas, norintis tik prisijungti prie sistemos, turi parsisiųsti ir dashboard’o, ir admin panelės, ir ataskaitos generatoriaus kodą, nors to viso jam dar nereikia.
Code splitting – tai būdas išdalinti jūsų aplikacijos kodą į mažesnius gabalus, kurie kraunami tik tada, kai jų reikia. Skamba paprasta, bet įgyvendinimas turi savo niuansų.
Route-based splitting: pradėkite nuo akivaizdžiausio
Pats paprasčiausias ir efektyviausias būdas pradėti – dalinti kodą pagal route’us. Pagalvokite: kodėl vartotojas, kuris ką tik atsidarė jūsų aplikacijos pradžios puslapį, turėtų parsisiųsti visą settings puslapio kodą? Atsakymas – neturėtų.
React.lazy() ir Suspense komponentas čia tampa jūsų geriausiais draugais:
Štai ir viskas – jau turite veikiantį code splitting’ą. Bet čia slypi keletas spąstų, į kuriuos verta neiškristi.
Pirma, tas fallback prop’as Suspense komponente. Nemėginkite ten dėti sudėtingų komponentų su savo state’u ar effect’ais. Paprastas loader’is ar skeleton screen – tai viskas, ko reikia. Kodėl? Nes šis komponentas bus rodomas labai trumpai, ir jei jis pats pradės krautis duomenis ar darys kažką sudėtingo, tik pabloginsite situaciją.
Antra, būkite atsargūs su lazy() import’ais. Jei parašysite kažką panašaus į lazy(() => import('./pages/' + pageName)), webpack’as nebesugebės tinkamai optimizuoti ir gali sukurti chunk’ą kiekvienam failui tame kataloge.
Component-level splitting: kai reikia smulkesnės kontrolės
Route-based splitting’as puikus, bet kartais jums reikia dar smulkesnės kontrolės. Pavyzdžiui, turite puslapį su sunkiu modal’u, kuris atsidaro tik paspaudus mygtuką. Kodėl tas modal’o kodas turėtų būti įtrauktas į pradinį bundle’ą?
Šis triukas leidžia pradėti krauti komponentą dar prieš vartotojui paspaudžiant mygtuką. Dažniausiai tų kelių šimtų milisekundžių, kol vartotojas perkelia pelę ir paspaudžia, užtenka komponentui užsikrauti.
Bibliotekų splitting’as: kai lodash suėda pusę jūsų bundle’o
Viena didžiausių problemų, su kuriomis susiduriate – trečiųjų šalių bibliotekos. Importuojate vieną funkciją iš lodash, o gaunate visą biblioteką bundle’e. Yra keletas būdų tai išspręsti.
Pirmas būdas – naudokite named import’us iš specifinių paketų:
// Blogai
import _ from 'lodash';
const result = _.debounce(myFunc, 300);
// Gerai
import debounce from 'lodash/debounce';
const result = debounce(myFunc, 300);
Bet ne visos bibliotekos tai palaiko. Todėl webpack’e galite naudoti SplitChunksPlugin konfigūraciją:
Ši konfigūracija išskiria kiekvieną node_modules biblioteką į atskirą chunk’ą. Ar tai visada gera idėja? Ne. Jei turite 50 mažų bibliotekų, gausit 50 atskirų HTTP request’ų, o tai gali būti lėčiau nei vienas didelis failas.
Geresnė strategija – grupuoti bibliotekas pagal tai, kaip dažnai jos keičiasi:
Tokiu būdu React ir jo ekosistema (kuri keičiasi retai) bus atskirame chunk’e, kurį naršyklė galės ilgam cache’inti.
Dynamic imports su sąlygomis: kraukite tik tai, ko tikrai reikia
Kartais jums reikia krauti skirtingą kodą priklausomai nuo sąlygų. Pavyzdžiui, turite skirtingas formas skirtingiems vartotojų tipams, arba skirtingus chart’ų komponentus priklausomai nuo pasirinkto vaizdualizacijos tipo.
async function loadChart(chartType) {
let ChartComponent;
switch(chartType) {
case 'bar':
ChartComponent = await import('./charts/BarChart');
break;
case 'line':
ChartComponent = await import('./charts/LineChart');
break;
case 'pie':
ChartComponent = await import('./charts/PieChart');
break;
default:
ChartComponent = await import('./charts/DefaultChart');
}
return ChartComponent.default;
}
function ChartContainer({ type, data }) {
const [Chart, setChart] = useState(null);
useEffect(() => {
loadChart(type).then(setChart);
}, [type]);
if (!Chart) return ;
return ;
}
Šis pattern’as ypač naudingas, kai turite daug skirtingų komponentų variantų, bet vartotojas naudoja tik vieną ar kelis iš jų.
Dar vienas praktiškas pavyzdys – feature flags. Jei turite naują funkcionalumą, kuris prieinamas tik daliai vartotojų, kodėl visi turėtų jį parsisiųsti?
function FeatureContainer({ user }) {
const [NewFeature, setNewFeature] = useState(null);
useEffect(() => {
if (user.hasAccessToNewFeature) {
import('./features/NewFeature').then(module => {
setNewFeature(() => module.default);
});
}
}, [user.hasAccessToNewFeature]);
if (user.hasAccessToNewFeature && !NewFeature) {
return ;
}
return NewFeature ? : ;
}
Prefetching ir preloading: būkite vienu žingsniu priekyje
Webpack palaiko magic comments, kurie leidžia kontroliuoti, kaip chunk’ai kraunami. Du svarbiausi – webpackPrefetch ir webpackPreload.
Skirtumas tarp jų esminis. Prefetch sako naršyklei: „Šis kodas gali prireikti ateityje, tai užkrauk jį, kai turėsi laisvo laiko”. Preload sako: „Šis kodas prireiks labai greitai, pradėk krauti dabar”.
Naudokite prefetch route’ams, kuriuos vartotojas tikriausiai aplankys. Pavyzdžiui, jei vartotojas prisijungė prie sistemos, labai tikėtina, kad jis eis į dashboard’ą, tad galite prefetch’inti tą route’ą jau login puslapyje.
function LoginPage() {
useEffect(() => {
// Prefetch dashboard po sėkmingo prisijungimo
const prefetchDashboard = () => import('./pages/Dashboard');
// Palaukiam šiek tiek, kad neperkrautume tinklo login metu
const timer = setTimeout(prefetchDashboard, 2000);
return () => clearTimeout(timer);
}, []);
return ;
}
Monitorinimas ir optimizavimas: kaip suprasti, ar jūsų strategija veikia
Viskas gražu teorijoje, bet kaip žinoti, ar jūsų code splitting strategija tikrai veikia? Reikia matuoti.
Pirmas įrankis – webpack bundle analyzer. Jį įdiegus, pamatysite vizualią jūsų bundle’o reprezentaciją:
Paleiskite build’ą, ir pamatysite interaktyvią tree map, kur galėsite pamatyti, kas užima daugiausiai vietos jūsų bundle’e. Dažnai rezultatai būna netikėti – paaiškėja, kad kažkokia mažytė bibliotėkėlė traukia puse megabaito dependencies.
Antras svarbus dalykas – real user monitoring. Chrome DevTools Performance tab puikus, bet jis rodo tik jūsų kompiuteryje. Realūs vartotojai turi lėtesnius įrenginius ir prastesnį internetą.
// Paprasta metrika, kuri siunčia duomenis į jūsų analytics
if ('PerformanceObserver' in window) {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.entryType === 'navigation') {
// Siųskite šiuos duomenis į analytics
console.log('DOMContentLoaded:', entry.domContentLoadedEventEnd);
console.log('Load complete:', entry.loadEventEnd);
}
}
});
observer.observe({ entryTypes: ['navigation'] });
}
Dar vienas naudingas metric’as – Time to Interactive (TTI). Tai laikas, per kurį puslapis tampa pilnai interaktyvus. Galite naudoti Lighthouse CI savo CI/CD pipeline’e, kad automatiškai tikrintumėte, ar jūsų pakeitimai nepablogino performance.
Dažniausios klaidos ir kaip jų išvengti
Per daug smulkaus splitting’o – tai viena didžiausių klaidų. Matau projektus, kur kiekvienas komponentas yra lazy loaded. Rezultatas? Šimtai mažų chunk’ų, kurie sukuria daugybę HTTP request’ų. HTTP/2 šiek tiek padeda, bet vis tiek yra overhead.
Gera taisyklė – chunk’as turėtų būti bent 20-30KB (gzipped). Jei mažesnis, greičiausiai nėra prasmės jo atskirti.
Kita problema – suspense boundary’ai. Jei dėsite Suspense per aukštai komponentų medyje, vartotojas matys loading state’ą visam puslapiui, nors kraunasi tik maža dalis. Jei per žemai – gausite „loading waterfall”, kai vienas po kito kraunasi skirtingi komponentai.
// Blogai - per aukštas Suspense
function App() {
return (
}>
);
}
// Gerai - granular Suspense
function App() {
return (
<>
}>
>
);
}
Dar viena klaida – ignoruoti error boundary’us. Lazy loaded komponentai gali fail’inti (network klaidos, 404, ir t.t.), ir jums reikia to handle’inti:
Code splitting nėra vienkartinis veiksmas – tai nuolatinis optimizavimo procesas. Pradėkite nuo route-based splitting, nes tai duoda didžiausią efektą su mažiausiomis pastangomis. Paskui žiūrėkite į sunkius komponentus, kurie naudojami retai. Optimizuokite bibliotekų import’us. Naudokite prefetching protingai.
Svarbiausia – matuokite. Bundle analyzer ir real user metrics parodo tikrąjį vaizdą. Kartais paaiškėja, kad jūsų kruopščiai optimizuotas code splitting neduoda jokios naudos, nes problema yra API response laikas arba neoptimizuoti paveikslėliai.
Ir nepamirškite – greitis yra feature. Kiekviena sutaupyta sekundė loading time reiškia mažesnį bounce rate ir laimingesnius vartotojus. O laimingi vartotojai – tai tas, dėl ko mes visa tai darome, ar ne?
Pradėkite nuo mažų žingsnių, testuokite kiekvieną pakeitimą, ir netrukus jūsų React aplikacija krausis ne kaip senas traktorius, o kaip Tesla – greitai, sklandžiai ir efektyviai.