Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Code splitting

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.

Czym jest code splitting?

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.

Dlaczego potrzebujemy code splitting?

Wyobraźmy sobie, że tworzymy kosmiczną aplikację z wieloma modułami:

  • Panel kontrolny misji
  • System monitorowania astronautów
  • Mapa galaktyki
  • Encyklopedia odkrytych planet
  • Symulator lotu
  • Centrum komunikacji międzygwiezdnej

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:

  • Dłuższy czas ładowania pierwszej strony
  • Większe zużycie pamięci przeglądarki
  • Większe koszty transferu danych (szczególnie istotne na urządzeniach mobilnych)
  • Niepotrzebne ładowanie i parsowanie kodu, który nie jest używany

Korzyści z code splitting

  • Szybsze początkowe ładowanie - krótszy czas do interaktywności (TTI - Time To Interactive)
  • Lepsza wydajność na urządzeniach mobilnych - mniejsze zużycie pamięci i procesora
  • Priorytetyzacja krytycznych ścieżek - najważniejsze części aplikacji ładują się pierwsze
  • Lepsza pamięć podręczna - mniejsze części łatwiej się buforują i aktualizują
  • Lepsze doświadczenie użytkownika - aplikacja jest responsywna szybciej

Implementacja code splitting w React

React wspiera code splitting za pomocą dynamicznego importu i komponentu

React.lazy
. Zobaczmy, jak to działa:

1. Dynamiczny import

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}

2. React.lazy

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:

  1. Używamy
    React.lazy
    do dynamicznego importowania komponentów
  2. Opakowujemy komponenty w
    Suspense
    , aby pokazać fallback podczas ładowania
  3. Komponenty są ładowane tylko wtedy, gdy są potrzebne (gdy użytkownik zmienia widok)

Suspense

Suspense
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>

Zagnieżdżanie 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}

Code splitting w oparciu o routing

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}

Code splitting bibliotek

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}

Preloading

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}

Named exports i code splitting

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);

Strategie dzielenia kodu

1. Podział według tras (Routes)

Najczęstszy i najłatwiejszy sposób - każda strona to oddzielny chunk kodu.

2. Podział według funkcjonalności (Feature-based)

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}

3. Podział według priorytetu (Priority-based)

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}

4. Podział przy użyciu bibliotek (Vendor splitting)

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};

Analiza i debugowanie code splitting

Analizowanie wielkości paczek

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}

Debugowanie problemów z code-splitting

Częste problemy z code splitting i ich rozwiązania:

  1. Komponenty migają podczas ładowania

    • Rozwiązanie: Zaprojektuj fallback tak, aby pasował do układu końcowego komponentu
  2. Za dużo małych chunków

    • Rozwiązanie: Grupuj powiązane komponenty w jednym dynamic import
  3. Problemy z SSR (Server-Side Rendering)

    • Rozwiązanie: Użyj
      loadable-components
      zamiast
      React.lazy
      dla SSR

Dobre praktyki w zakresie code splitting

  1. Nie 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

  2. Analizuj swoje paczki - regularnie monitoruj rozmiar i zawartość bundli

  3. 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));
  4. 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ść

  5. Monitoruj wpływ na wydajność - używaj narzędzi takich jak Lighthouse, aby mierzyć czasy ładowania i TTI (Time To Interactive)

Code splitting a TypeScript

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}

Podsumowanie

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ów
  • Dynamiczny
    import()
    dla modułów
  • Strategie podziału oparte na trasach, funkcjonalności lub priorytecie
  • Narzędzia analizy bundli pomagające w podejmowaniu decyzji

Wdraż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.

Przejdź do CodeWorlds