W miarę jak nasza kosmiczna aplikacja React rośnie, plik JavaScript, który ładuje się w przeglądarce, również się powiększa. Może to prowadzić do dłuższych czasów ładowania strony, większego zużycia pamięci i ogólnie słabszej wydajności. Code splitting (podział kodu) to technika, która pozwala na "podział" aplikacji na mniejsze części, które mogą być ładowane dynamicznie, tylko wtedy, gdy są potrzebne.
Code splitting to technika, która dzieli duży pakiet kodu JavaScript na mniejsze kawałki (tzw. "chunki"), które mogą być ładowane na żądanie lub równolegle. Zamiast wysyłać całą aplikację do użytkownika od razu, przesyłamy tylko to, co jest potrzebne do początkowego renderowania, a reszta jest ładowana w tle lub gdy jest potrzebna.
Jest to jak start kosmicznej misji - nie wysyłamy wszystkich zasobów od razu, ale etapami: najpierw statek główny, potem moduły dodatkowe, a na końcu mniejsze narzędzia i wyposażenie, gdy stają się potrzebne.
Wyobraźmy sobie, że tworzymy kosmiczną aplikację z wieloma modułami:
Jeśli zbudujemy całą aplikację jako jeden pakiet, użytkownik musi pobrać cały ten kod, nawet jeśli chce tylko sprawdzić mapę galaktyki. W rezultacie:
React wspiera code splitting za pomocą dynamicznego importu i komponentu
React.lazy. Zobaczmy, jak to działa:Funkcja
import() to standardowa funkcja ES, która pozwala na dynamiczne importowanie modułów. Zamiast ładować wszystkie zależności na górze pliku, możemy załadować je tylko wtedy, gdy są potrzebne:1// Bez code splitting
2import { calculateOrbit } from './complexOrbitCalculations';
3
4function OrbitCalculator({ planetData }) {
5 const orbitInfo = calculateOrbit(planetData);
6 return <div>{/* Renderowanie wyników */}</div>;
7}
8
9// Z code splitting
10function OrbitCalculator({ planetData }) {
11 const [orbitInfo, setOrbitInfo] = useState(null);
12 const [loading, setLoading] = useState(true);
13
14 useEffect(() => {
15 // Dynamiczny import modułu dopiero, gdy komponent się zamontuje
16 import('./complexOrbitCalculations')
17 .then(module => {
18 const result = module.calculateOrbit(planetData);
19 setOrbitInfo(result);
20 setLoading(false);
21 });
22 }, [planetData]);
23
24 if (loading) return <p>Obliczanie orbity...</p>;
25 return <div>{/* Renderowanie wyników */}</div>;
26}React.lazy to funkcja, która pozwala na renderowanie dynamicznego importu jako zwykłego komponentu. Działa tylko z domyślnymi eksportami (default exports).1import React, { Suspense } from 'react';
2
3// Zwykły import
4// import GalaxyMap from './GalaxyMap';
5
6// Leniwy import
7const GalaxyMap = React.lazy(() => import('./GalaxyMap'));
8const PlanetDetails = React.lazy(() => import('./PlanetDetails'));
9const SpaceshipControls = React.lazy(() => import('./SpaceshipControls'));
10
11function SpaceExplorer() {
12 const [view, setView] = useState('map');
13
14 return (
15 <div className="space-explorer">
16 <nav>
17 <button onClick={() => setView('map')}>Mapa Galaktyki</button>
18 <button onClick={() => setView('planets')}>Szczegóły Planet</button>
19 <button onClick={() => setView('controls')}>Panel Sterowania</button>
20 </nav>
21
22 <Suspense fallback={<div>Ładowanie modułu...</div>}>
23 {view === 'map' && <GalaxyMap />}
24 {view === 'planets' && <PlanetDetails />}
25 {view === 'controls' && <SpaceshipControls />}
26 </Suspense>
27 </div>
28 );
29}W powyższym przykładzie:
React.lazy do dynamicznego importowania komponentówSuspense, aby pokazać fallback podczas ładowaniaSuspense to komponent React, który pozwala na "zawieszenie" renderowania do momentu, aż pewne warunki zostają spełnione (np. dane są załadowane lub kod komponenty jest dostępny).Suspense przyjmuje prop
fallback, który jest renderowany podczas oczekiwania:1<Suspense fallback={<LoadingSpinner message="Przygotowywanie modułu..." />}>
2 <LazilyLoadedComponent />
3</Suspense>Suspense może obsłużyć wiele komponentów Lazy na raz:
1<Suspense fallback={<LoadingSpinner />}>
2 <LazyComponent1 />
3 <div>
4 <LazyComponent2 />
5 </div>
6 <LazyComponent3 />
7</Suspense>Możemy również zagnieżdżać komponenty Suspense, aby uzyskać bardziej granularną kontrolę nad tym, co jest wyświetlane podczas ładowania:
1function SpaceStation() {
2 return (
3 <div>
4 <h1>Stacja Kosmiczna Alpha</h1>
5
6 {/* Główny układ ładuje się natychmiast */}
7 <StationLayout />
8
9 {/* Mapa ładuje się z własnym fallbackiem */}
10 <Suspense fallback={<p>Wczytywanie mapy stacji...</p>}>
11 <StationMap />
12 </Suspense>
13
14 {/* Panel kontrolny ma zagnieżdżone komponenty lazy */}
15 <Suspense fallback={<p>Przygotowywanie panelu kontrolnego...</p>}>
16 <ControlPanel>
17 <Suspense fallback={<p>Ładowanie systemów podtrzymywania życia...</p>}>
18 <LifeSupportMonitor />
19 </Suspense>
20
21 <Suspense fallback={<p>Ładowanie kontroli silników...</p>}>
22 <EngineControls />
23 </Suspense>
24 </ControlPanel>
25 </Suspense>
26 </div>
27 );
28}Jednym z najczęstszych sposobów zastosowania code splitting jest podział kodu na podstawie tras (routes). Każda strona lub widok staje się oddzielnym chunkiem kodu:
1import React, { Suspense, lazy } from 'react';
2import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
3import Navbar from './Navbar';
4import LoadingPage from './LoadingPage';
5
6// Leniwe ładowanie stron
7const MissionControl = lazy(() => import('./pages/MissionControl'));
8const GalaxyMap = lazy(() => import('./pages/GalaxyMap'));
9const CrewManagement = lazy(() => import('./pages/CrewManagement'));
10const ResearchDatabase = lazy(() => import('./pages/ResearchDatabase'));
11const SpaceshipConfig = lazy(() => import('./pages/SpaceshipConfig'));
12
13function App() {
14 return (
15 <Router>
16 <Navbar />
17 <Suspense fallback={<LoadingPage />}>
18 <Switch>
19 <Route exact path="/" component={MissionControl} />
20 <Route path="/galaxy-map" component={GalaxyMap} />
21 <Route path="/crew" component={CrewManagement} />
22 <Route path="/research" component={ResearchDatabase} />
23 <Route path="/spaceship" component={SpaceshipConfig} />
24 </Switch>
25 </Suspense>
26 </Router>
27 );
28}Możemy również stosować code splitting dla dużych bibliotek, które nie są potrzebne natychmiast:
1import React, { useState } from 'react';
2
3function DataVisualizer({ data }) {
4 const [charts, setCharts] = useState(null);
5 const [showCharts, setShowCharts] = useState(false);
6
7 const loadChartsLibrary = async () => {
8 // Załaduj bibliotekę dopiero, gdy użytkownik chce zobaczyć wykresy
9 const D3Module = await import('d3');
10 const ChartModule = await import('./ChartComponent');
11
12 setCharts({
13 d3: D3Module.default,
14 ChartComponent: ChartModule.default
15 });
16 };
17
18 const handleShowCharts = () => {
19 if (!charts) {
20 setShowCharts(true);
21 loadChartsLibrary();
22 } else {
23 setShowCharts(true);
24 }
25 };
26
27 return (
28 <div className="data-visualizer">
29 <h2>Dane eksploracji kosmicznej</h2>
30 <button onClick={handleShowCharts}>
31 {showCharts ? 'Ukryj wykresy' : 'Pokaż wykresy'}
32 </button>
33
34 {showCharts && (
35 <>
36 {charts ? (
37 <charts.ChartComponent data={data} d3={charts.d3} />
38 ) : (
39 <p>Ładowanie zaawansowanych wykresów...</p>
40 )}
41 </>
42 )}
43 </div>
44 );
45}Czasami wiemy, że komponent będzie potrzebny wkrótce (np. gdy użytkownik najedzie na przycisk, który pokaże modal). Możemy zacząć ładować moduł wcześniej:
1const PlanetExplorer = React.lazy(() => import('./PlanetExplorer'));
2
3function PlanetInfoButton({ planetId }) {
4 const [showExplorer, setShowExplorer] = useState(false);
5
6 const handleMouseEnter = () => {
7 // Rozpocznij pobieranie modułu, gdy użytkownik najedzie na przycisk
8 const PlanetExplorerModule = import('./PlanetExplorer');
9 };
10
11 return (
12 <>
13 <button
14 onMouseEnter={handleMouseEnter}
15 onClick={() => setShowExplorer(true)}
16 >
17 Eksploruj planetę {planetId}
18 </button>
19
20 {showExplorer && (
21 <Suspense fallback={<p>Przygotowywanie eksploratora...</p>}>
22 <PlanetExplorer planetId={planetId} />
23 </Suspense>
24 )}
25 </>
26 );
27}React.lazy działa tylko z domyślnymi eksportami (default exports). Jeśli chcemy użyć nazwanych eksportów (named exports), musimy utworzyć pośredni moduł:1// MathUtils.js
2export const calculateDistance = (a, b) => { /* ... */ };
3export const calculateVelocity = (distance, time) => { /* ... */ };
4export const calculateAcceleration = (velocity, time) => { /* ... */ };
5
6// DistanceCalculator.js (moduł pośredni)
7export { calculateDistance } from './MathUtils';
8export default calculateDistance;
9
10// Użycie
11const DistanceCalculator = React.lazy(() => import('./DistanceCalculator'));Alternatywnie, możemy ręcznie obsłużyć import i wyodrębnić żądany eksport:
1const calculateVelocity = React.lazy(() =>
2 import('./MathUtils').then(module => ({ default: module.calculateVelocity }))
3);Najczęstszy i najłatwiejszy sposób - każda strona to oddzielny chunk kodu.
Komponenty związane z określoną funkcjonalnością (np. panel administracyjny, zaawansowana analityka) są grupowane razem.
1// Ładowanie całego modułu administratora na żądanie
2const AdminDashboard = React.lazy(() => import('./admin/Dashboard'));
3
4function App() {
5 const [isAdmin, setIsAdmin] = useState(false);
6
7 // isAdmin jest ustawiane po logowaniu użytkownika
8
9 return (
10 <div>
11 {isAdmin ? (
12 <Suspense fallback={<p>Ładowanie panelu admina...</p>}>
13 <AdminDashboard />
14 </Suspense>
15 ) : (
16 <UserDashboard />
17 )}
18 </div>
19 );
20}Krytyczne komponenty ładowane od razu, mniej ważne leniwie.
1// Krytyczne komponenty importowane standardowo
2import CriticalSystemStatus from './CriticalSystemStatus';
3import NavigationControls from './NavigationControls';
4
5// Mniej istotne komponenty ładowane leniwie
6const SystemLogs = React.lazy(() => import('./SystemLogs'));
7const CrewSchedule = React.lazy(() => import('./CrewSchedule'));
8const MaintenanceStatus = React.lazy(() => import('./MaintenanceStatus'));
9
10function SpaceshipDashboard() {
11 return (
12 <div className="dashboard">
13 <div className="critical-section">
14 <CriticalSystemStatus />
15 <NavigationControls />
16 </div>
17
18 <Suspense fallback={<p>Ładowanie dodatkowych modułów...</p>}>
19 <div className="secondary-section">
20 <SystemLogs />
21 <CrewSchedule />
22 <MaintenanceStatus />
23 </div>
24 </Suspense>
25 </div>
26 );
27}Dzielenie dużych bibliotek zewnętrznych na oddzielne chunki.
W webpack można to osiągnąć za pomocą konfiguracji
splitChunks:1module.exports = {
2 //...
3 optimization: {
4 splitChunks: {
5 chunks: 'all',
6 maxInitialRequests: Infinity,
7 minSize: 0,
8 cacheGroups: {
9 vendor: {
10 test: /[\/]node_modules[\/]/,
11 name(module) {
12 // get the name. E.g. node_modules/packageName/sub/path
13 // or node_modules/packageName
14 const packageName = module.context.match(
15 /[\/]node_modules[\/](.*?)([\/]|$)/
16 )[1];
17
18 // npm package names are URL-safe, but some servers don't like @ symbols
19 return `npm.${packageName.replace('@', '')}`;
20 },
21 },
22 },
23 },
24 },
25};Narzędzia takie jak "webpack-bundle-analyzer" lub "source-map-explorer" pomagają wizualizować zawartość paczek i identyfikować możliwości podziału:
1# Instalacja
2npm install --save-dev webpack-bundle-analyzer
3
4# Konfiguracja w webpack.config.js
5const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
6
7module.exports = {
8 plugins: [
9 new BundleAnalyzerPlugin()
10 ]
11}Częste problemy z code splitting i ich rozwiązania:
Komponenty migają podczas ładowania
Za dużo małych chunków
Problemy z SSR (Server-Side Rendering)
loadable-components zamiast React.lazy dla SSRNie przesadzaj z podziałem - zbyt wiele małych chunków może prowadzić do problemów z wydajnością ze względu na liczne żądania HTTP
Analizuj swoje paczki - regularnie monitoruj rozmiar i zawartość bundli
Używaj Magic Comments w webpack do lepszej kontroli nad chunkami:
1// Nazwany chunk
2const HomeComponent = React.lazy(() => import(
3 /* webpackChunkName: "home" */
4 './HomeComponent'
5));
6
7// Prefetch - zasugeruj przeglądarce, że ten zasób będzie potrzebny
8const SettingsComponent = React.lazy(() => import(
9 /* webpackChunkName: "settings" */
10 /* webpackPrefetch: true */
11 './SettingsComponent'
12));
13
14// Preload - załaduj ten zasób z wysokim priorytetem
15const ProfileComponent = React.lazy(() => import(
16 /* webpackChunkName: "profile" */
17 /* webpackPreload: true */
18 './ProfileComponent'
19));Używaj code splitting w połączeniu z pamięcią podręczną - prawidłowo skonfigurowane nagłówki cache dla chunków mogą znacznie poprawić wydajność
Monitoruj wpływ na wydajność - używaj narzędzi takich jak Lighthouse, aby mierzyć czasy ładowania i TTI (Time To Interactive)
TypeScript działa dobrze z code splitting, ale wymaga kilku dodatkowych kroków:
1// Upewnij się, że w tsconfig.json jest ustawione:
2// "module": "esnext"
3
4// Dynamiczny import z typowaniem
5const DataVisualization = React.lazy(() =>
6 import('./DataVisualization') as Promise<{ default: React.ComponentType<DataVisProps> }>
7);
8
9interface DataVisProps {
10 data: Array<DataPoint>;
11 width: number;
12 height: number;
13}
14
15interface DataPoint {
16 x: number;
17 y: number;
18 label: string;
19}
20
21function Dashboard() {
22 const [data, setData] = useState<Array<DataPoint>>([]);
23
24 return (
25 <div>
26 <h1>Panel misji kosmicznej</h1>
27
28 <Suspense fallback={<div>Ładowanie wizualizacji...</div>}>
29 <DataVisualization
30 data={data}
31 width={800}
32 height={400}
33 />
34 </Suspense>
35 </div>
36 );
37}Code splitting to potężna technika optymalizacji, która pozwala na znaczne polepszenie wydajności aplikacji React, szczególnie dla większych projektów. Dzięki podziałowi kodu użytkownicy ładują tylko to, czego potrzebują, co prowadzi do szybszego uruchomienia aplikacji i lepszego doświadczenia użytkownika.
Kluczowe narzędzia i metody to:
React.lazy() i Suspense dla komponentówimport() dla modułówWdrażając code splitting w naszej kosmicznej aplikacji, możemy zapewnić, że użytkownicy szybko uzyskają dostęp do najważniejszych funkcji, a dodatkowe zasoby będą ładowane wtedy, gdy będą potrzebne - podobnie jak w wieloetapowej misji kosmicznej, gdzie każdy moduł jest dostarczany we właściwym czasie.