Kurs JavaScript i React · Moduł 6: React Router
Lazy loading komponentów w kosmicznej podróży
W tej lekcji8
Podczas długiej międzygalaktycznej podróży, nie potrzebujemy od razu ładować całego wyposażenia na statek kosmiczny. Podobnie w aplikacjach React, szczególnie tych większych, nie potrzebujemy ładować wszystkich komponentów od razu podczas inicjalizacji aplikacji. Oto gdzie przychodzi nam z pomocą leniwe ładowanie (lazy loading).
Czym jest Lazy Loading?
Lazy loading to technika optymalizacji wydajności, która polega na ładowaniu komponentów, modułów lub zasobów tylko wtedy, gdy są faktycznie potrzebne, zamiast ładować je z góry podczas inicjalizacji aplikacji. W React, realizujemy to za pomocą funkcji React.lazy() w połączeniu z Suspense.
Podstawy Lazy Loading w React
Aby zaimplementować lazy loading komponentu, zamiast standardowego importowania komponentu:
1import HeavyDashboard from './HeavyDashboard';Używamy funkcji React.lazy() z dynamicznym importem:
1import React, { lazy, Suspense } from 'react';
2
3// Leniwe ładowanie komponentu
4const HeavyDashboard = lazy(() => import('./HeavyDashboard'));
5
6function App() {
7 return (
8 <div>
9 <h1>Centrum Kontroli Lotu</h1>
10
11 {/* Suspense zapewnia komponent zastępczy podczas ładowania */}
12 <Suspense fallback={<div>Ładowanie panelu sterowania...</div>}>
13 <HeavyDashboard />
14 </Suspense>
15 </div>
16 );
17}W tym przykładzie:
lazy()przyjmuje funkcję, która musi wywołać dynamicznyimport()Suspensewyświetla fallback (komponent zastępczy) podczas ładowania leniwego komponentu- Faktyczny komponent
HeavyDashboardzaładuje się dopiero wtedy, gdy będzie renderowany
Lazy Loading w React Router
W aplikacjach wykorzystujących React Router, możemy leniwie ładować komponenty dla poszczególnych tras. Dzięki temu początkowy bundle aplikacji będzie mniejszy, a użytkownik będzie pobierać kod tylko dla tych części aplikacji, które odwiedza:
1import React, { lazy, Suspense } from 'react';
2import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
3
4// Komponenty ładowane leniwie
5const MissionControl = lazy(() => import('./MissionControl'));
6const SpaceshipSettings = lazy(() => import('./SpaceshipSettings'));
7const GalacticMap = lazy(() => import('./GalacticMap'));
8const CrewQuarters = lazy(() => import('./CrewQuarters'));
9
10// Komponent ładowany zawsze (mały i często używany)
11import NavigationBar from './NavigationBar';
12
13function App() {
14 return (
15 <Router>
16 <NavigationBar />
17
18 <Suspense fallback={<div className="loading-screen">Przygotowanie modułu...</div>}>
19 <Routes>
20 <Route path="/" element={<HomePage />} />
21 <Route path="/mission-control" element={<MissionControl />} />
22 <Route path="/spaceship-settings" element={<SpaceshipSettings />} />
23 <Route path="/galactic-map" element={<GalacticMap />} />
24 <Route path="/crew-quarters" element={<CrewQuarters />} />
25 </Routes>
26 </Suspense>
27 </Router>
28 );
29}W tym przykładzie tylko komponenty dla aktualnie odwiedzanej trasy będą ładowane. Możemy również dodać indywidualne komponenty Suspense dla każdej trasy, jeśli chcemy różne wskaźniki ładowania:
1<Routes>
2 <Route path="/" element={<HomePage />} />
3 <Route
4 path="/mission-control"
5 element={
6 <Suspense fallback={<div>Inicjalizacja panelu kontroli misji...</div>}>
7 <MissionControl />
8 </Suspense>
9 }
10 />
11 <Route
12 path="/galactic-map"
13 element={
14 <Suspense fallback={<div>Ładowanie mapy galaktycznej...</div>}>
15 <GalacticMap />
16 </Suspense>
17 }
18 />
19</Routes>Zaawansowane techniki Lazy Loading
Prefetching (wstępne ładowanie)
Czasami wiemy z dużym prawdopodobieństwem, że użytkownik odwiedzi określoną trasę. Możemy wtedy wstępnie załadować komponent, zanim użytkownik faktycznie kliknie w link:
1import { lazy } from 'react';
2
3// Deklaracja leniwego komponentu
4const GalacticMap = lazy(() => import('./GalacticMap'));
5
6// Komponent nawigacyjny z prefetchingiem
7function NavigationLink({ to, children }) {
8 // Funkcja do wstępnego ładowania
9 const prefetchComponent = () => {
10 // Dynamiczny import jako Promise - rozpoczyna ładowanie
11 import('./GalacticMap');
12 };
13
14 return (
15 <Link
16 to={to}
17 onMouseOver={prefetchComponent} // Rozpocznij ładowanie po najechaniu myszą
18 onFocus={prefetchComponent} // Dla dostępności, ładowanie po fokusie
19 >
20 {children}
21 </Link>
22 );
23}
24
25// Użycie w komponencie
26function Navigation() {
27 return (
28 <nav>
29 <NavigationLink to="/galactic-map">Mapa Galaktyczna</NavigationLink>
30 </nav>
31 );
32}Grupowanie tras (route-based code splitting)
Możemy grupować powiązane ze sobą lazy-loaded komponenty, aby zmniejszyć liczbę oddzielnych żądań sieciowych:
1// Grupowanie komponentów związanych z kokpitem
2const CockpitModule = lazy(() => import('./modules/CockpitModule'));
3
4// Używanie zgrupowanego modułu w routingu
5<Route path="/cockpit/*" element={<CockpitModule />} />
6
7// W CockpitModule.js mamy wewnętrzne route'y
8function CockpitModule() {
9 return (
10 <Routes>
11 <Route path="/" element={<CockpitOverview />} />
12 <Route path="/navigation" element={<NavigationControls />} />
13 <Route path="/engines" element={<EngineControls />} />
14 <Route path="/life-support" element={<LifeSupportSystems />} />
15 </Routes>
16 );
17}W tym podejściu, wszystkie komponenty kokpitu zostaną pobrane jako jeden chunk kodu, gdy użytkownik wejdzie na dowolną trasę /cockpit/*.
Obsługa błędów ładowania
W kosmosie zawsze musimy być przygotowani na nieprzewidziane sytuacje. W przypadku lazy loadingu, może się zdarzyć, że próba ładowania komponentu się nie powiedzie (np. z powodu problemów z siecią):
1import React, { lazy, Suspense } from 'react';
2import { ErrorBoundary } from 'react-error-boundary';
3
4const GalacticMap = lazy(() => import('./GalacticMap'));
5
6function ErrorFallback({ error, resetErrorBoundary }) {
7 return (
8 <div className="error-container">
9 <h2>Napotkano problem podczas ładowania modułu</h2>
10 <p>Błąd: {error.message}</p>
11 <button onClick={resetErrorBoundary}>
12 Spróbuj ponownie
13 </button>
14 </div>
15 );
16}
17
18function MapSection() {
19 return (
20 <ErrorBoundary
21 FallbackComponent={ErrorFallback}
22 onReset={() => {
23 // Opcjonalna logika resetowania stanu
24 }}
25 >
26 <Suspense fallback={<div>Ładowanie mapy galaktycznej...</div>}>
27 <GalacticMap />
28 </Suspense>
29 </ErrorBoundary>
30 );
31}Kiedy warto używać Lazy Loading?
Lazy loading jest szczególnie przydatny w następujących przypadkach:
Duże aplikacje z wieloma komponentami, gdzie użytkownik zazwyczaj korzysta tylko z niewielkiej części funkcjonalności podczas jednej sesji
Rzadko używane funkcje - na przykład, panel administratora, który jest używany tylko przez kilku użytkowników
Ciężkie zależności - komponenty, które wykorzystują duże biblioteki (np. edytory tekstu, mapy, wykresy)
Alternatywne ścieżki użytkownika - gdy różni użytkownicy korzystają z różnych części aplikacji w zależności od ich roli
Najlepsze praktyki
1. Umieść Suspense wyżej w drzewie komponentów
Zamiast owijać każdy lazy-loaded komponent w osobny Suspense, często lepiej jest umieścić jeden Suspense wyżej w drzewie:
1function App() {
2 return (
3 <Suspense fallback={<GlobalLoadingSpinner />}>
4 <Router>
5 <Routes>
6 {/* Lazy-loaded routes */}
7 </Routes>
8 </Router>
9 </Suspense>
10 );
11}2. Stosuj odpowiednie granice podziału kodu (code splitting boundaries)
Dziel kod wzdłuż naturalnych granic aplikacji:
- Podział według tras (routingu)
- Podział według modułów funkcjonalnych
- Podział między częścią publiczną i prywatną aplikacji
3. Unikaj zbyt drobnego podziału
Zbyt drobny podział kodu może prowadzić do wielu małych żądań sieciowych, co może być mniej wydajne niż kilka większych. Znajdź złoty środek.
4. Zawsze dostarczaj dobry fallback
Upewnij się, że fallback w Suspense jest wizualnie spójny z resztą aplikacji i daje użytkownikowi wyraźną informację o stanie ładowania.
5. Testuj na rzeczywistych urządzeniach i wolnych połączeniach
Lazy loading może działać świetnie na szybkich połączeniach developerskich, ale zawsze testuj jak wygląda doświadczenie użytkownika na wolniejszych połączeniach.
Przykładowa implementacja
Oto przykład bardziej rozbudowanej aplikacji kosmicznej z lazy loadingiem:
1import React, { lazy, Suspense } from 'react';
2import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
3import { ErrorBoundary } from 'react-error-boundary';
4
5// Komponenty ładowane zawsze
6import MainLayout from './layouts/MainLayout';
7import LoadingScreen from './components/LoadingScreen';
8import ErrorFallback from './components/ErrorFallback';
9
10// Lazy-loaded pages
11const HomePage = lazy(() => import('./pages/HomePage'));
12const Dashboard = lazy(() => import('./pages/Dashboard'));
13const NavigationModule = lazy(() => import('./modules/NavigationModule'));
14const LifeSupportModule = lazy(() => import('./modules/LifeSupportModule'));
15const CommunicationModule = lazy(() => import('./modules/CommunicationModule'));
16const CrewModule = lazy(() => import('./modules/CrewModule'));
17const SettingsPage = lazy(() => import('./pages/SettingsPage'));
18const NotFoundPage = lazy(() => import('./pages/NotFoundPage'));
19
20function App() {
21 return (
22 <Router>
23 <ErrorBoundary FallbackComponent={ErrorFallback}>
24 <MainLayout>
25 <Suspense fallback={<LoadingScreen />}>
26 <Routes>
27 <Route path="/" element={<HomePage />} />
28 <Route path="/dashboard" element={<Dashboard />} />
29
30 {/* Zagnieżdżone moduły z własnymi trasami */}
31 <Route path="/navigation/*" element={<NavigationModule />} />
32 <Route path="/life-support/*" element={<LifeSupportModule />} />
33 <Route path="/communication/*" element={<CommunicationModule />} />
34 <Route path="/crew/*" element={<CrewModule />} />
35
36 <Route path="/settings" element={<SettingsPage />} />
37 <Route path="/404" element={<NotFoundPage />} />
38 <Route path="*" element={<Navigate to="/404" replace />} />
39 </Routes>
40 </Suspense>
41 </MainLayout>
42 </ErrorBoundary>
43 </Router>
44 );
45}
46
47export default App;Podsumowanie
Lazy loading to potężna technika optymalizacji React aplikacji, która pozwala:
- Zmniejszyć początkowy rozmiar paczki JavaScript (bundle)
- Skrócić czas ładowania aplikacji
- Poprawić doświadczenie użytkownika poprzez szybsze wstępne renderowanie
- Ładować kod tylko wtedy, gdy jest faktycznie potrzebny
Jest to jak inteligentne zarządzanie zasobami na statku kosmicznym - zamiast zabierać wszystko na początku podróży, transportujemy tylko to, co jest aktualnie potrzebne, oszczędzając paliwo i przyspieszając start.
Kod do tej lekcji: App.jsx
1import React from 'react';
2import { BrowserRouter, Routes, Route, Link, useSearchParams } from 'react-router-dom';
3
4// Lista produktów z filtrowaniem
5function ProductList() {
6 const [searchParams, setSearchParams] = useSearchParams();
7
8 const category = searchParams.get('category') || 'all';
9 const sortBy = searchParams.get('sort') || 'name';
10 const search = searchParams.get('search') || '';
11
12 const products = [
13 { id: 1, name: 'Skafander kosmiczny', category: 'equipment', price: 5000 },
14 { id: 2, name: 'Hełm astronauty', category: 'equipment', price: 1500 },
15 { id: 3, name: 'Rakieta modelowa', category: 'toys', price: 200 },
16 { id: 4, name: 'Teleskop', category: 'tools', price: 3000 },
17 { id: 5, name: 'Mapa galaktyki', category: 'tools', price: 500 },
18 { id: 6, name: 'Figurka astronauty', category: 'toys', price: 50 },
19 ];
20
21 // Filtrowanie
22 let filteredProducts = products;
23 if (category !== 'all') {
24 filteredProducts = filteredProducts.filter(p => p.category === category);
25 }
26 if (search) {
27 filteredProducts = filteredProducts.filter(p =>
28 p.name.toLowerCase().includes(search.toLowerCase())
29 );
30 }
31
32 // Sortowanie
33 filteredProducts.sort((a, b) => {
34 if (sortBy === 'price') return a.price - b.price;
35 return a.name.localeCompare(b.name);
36 });
37
38 const updateFilter = (key, value) => {
39 const newParams = new URLSearchParams(searchParams);
40 if (value) {
41 newParams.set(key, value);
42 } else {
43 newParams.delete(key);
44 }
45 setSearchParams(newParams);
46 };
47
48 return (
49 <div className="product-list">
50 <h1>Sklep kosmiczny</h1>
51
52 <div className="filters">
53 <div className="filter-group">
54 <label>Szukaj:</label>
55 <input
56 type="text"
57 value={search}
58 onChange={(e) => updateFilter('search', e.target.value)}
59 placeholder="Wpisz nazwę..."
60 />
61 </div>
62
63 <div className="filter-group">
64 <label>Kategoria:</label>
65 <select
66 value={category}
67 onChange={(e) => updateFilter('category', e.target.value)}
68 >
69 <option value="all">Wszystkie</option>
70 <option value="equipment">Sprzęt</option>
71 <option value="tools">Narzędzia</option>
72 <option value="toys">Zabawki</option>
73 </select>
74 </div>
75
76 <div className="filter-group">
77 <label>Sortuj:</label>
78 <select
79 value={sortBy}
80 onChange={(e) => updateFilter('sort', e.target.value)}
81 >
82 <option value="name">Nazwa</option>
83 <option value="price">Cena</option>
84 </select>
85 </div>
86 </div>
87
88 <div className="current-params">
89 <strong>URL params:</strong> {searchParams.toString() || '(brak)'}
90 </div>
91
92 <div className="products">
93 {filteredProducts.map(product => (
94 <div key={product.id} className="product">
95 <h3>{product.name}</h3>
96 <p className="category">{product.category}</p>
97 <p className="price">{product.price} zł</p>
98 </div>
99 ))}
100 {filteredProducts.length === 0 && (
101 <p className="no-results">Brak produktów spełniających kryteria</p>
102 )}
103 </div>
104 </div>
105 );
106}
107
108function App() {
109 return (
110 <BrowserRouter>
111 <div className="app">
112 <nav>
113 <Link to="/products">Wszystkie</Link>
114 <Link to="/products?category=equipment">Sprzęt</Link>
115 <Link to="/products?category=tools">Narzędzia</Link>
116 <Link to="/products?category=toys&sort=price">Zabawki (cena)</Link>
117 </nav>
118 <Routes>
119 <Route path="/" element={<ProductList />} />
120 <Route path="/products" element={<ProductList />} />
121 </Routes>
122 </div>
123 </BrowserRouter>
124 );
125}
126
127export default App;Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Co zwraca funkcja React.lazy() w React?
2. Jaka jest rola komponentu Suspense przy lazy loadingu?
Zadania praktyczne w grze
- Edytor kodu
Protected Routes
- Klikanie w kolejności
Ułóż składnię Route z dynamicznym parametrem:
- Edytor kodu
Lazy loading komponentów
- Układanie w pionie
Uporządkuj kroki lazy loadingu komponentu w React: