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

Profilowanie wydajności

Nasza kosmiczna podróż przez React zbliża się do końca, ale zanim wyruszymy na dalsze eksploracje, musimy upewnić się, że nasz statek kosmiczny działa na maksymalnych obrotach. W tym module skupimy się na profilowaniu wydajności aplikacji React - czyli identyfikacji i analizie "wąskich gardeł" w naszym kodzie.

Czym jest profilowanie wydajności?

Profilowanie wydajności to proces identyfikacji miejsc w aplikacji, które zużywają najwięcej zasobów (czas procesora, pamięć) i mogą prowadzić do wolnego działania interfejsu użytkownika. Jest to jak diagnozowanie silników statku kosmicznego, aby sprawdzić, które części nie działają wydajnie i mogą zostać zoptymalizowane.

W kontekście aplikacji React, profilowanie pozwala na:

  • Identyfikację komponentów, które renderują się zbyt często
  • Zlokalizowanie długo trwających operacji
  • Zrozumienie wzorców renderowania
  • Wykrycie niepotrzebnych aktualizacji stanu
  • Wskazanie miejsc wymagających optymalizacji

Narzędzia do profilowania React

1. React DevTools Profiler

React DevTools to rozszerzenie do przeglądarki, które pozwala na analizę wydajności aplikacji React. Zawiera ono zakładkę "Profiler", która jest najprostszym i najpotężniejszym narzędziem do profilowania aplikacji React.

Jak używać React DevTools Profiler:

  1. Instalacja React DevTools

  2. Uruchomienie profilowania:

    • Otwórz narzędzia deweloperskie w przeglądarce
    • Przejdź do zakładki "Profiler" w React DevTools
    • Kliknij przycisk nagrywania (czerwony kółko)
    • Wykonaj akcje w aplikacji, które chcesz profilować
    • Zatrzymaj nagrywanie
  3. Analiza wyników:

    • Flame Chart (Wykres płomienia) - pokazuje hierarchię renderowanych komponentów i czas ich renderowania
    • Ranked Chart (Wykres rankingowy) - sortuje komponenty według czasu renderowania
    • Component Chart (Wykres komponentów) - pokazuje, które komponenty zostały przerenderowane w każdej aktualizacji

Interpretacja kolorów w Profilerze:

  • Szary - komponent nie renderował się podczas aktualizacji
  • Żółty/Pomarańczowy/Czerwony - komponent renderował się, a kolor wskazuje, jak długo to trwało (im bardziej czerwony, tym dłużej)
  • Czarny - komponent był przyczyną aktualizacji (np. przez zmianę stanu)

2. Chrome Performance Panel

Chrome DevTools zawiera panel Performance, który pozwala na bardziej ogólne profilowanie wydajności strony:

  1. Uruchomienie profilowania:

    • Otwórz DevTools w Chrome (F12)
    • Przejdź do zakładki "Performance"
    • Kliknij przycisk nagrywania
    • Wykonaj akcje w aplikacji
    • Zatrzymaj nagrywanie
  2. Analizuj wyniki:

    • Main - pokazuje aktywność w głównym wątku (gdzie dzieje się większość pracy JS)
    • Frames - pokazuje ilość klatek na sekundę (FPS)
    • Network - pokazuje aktywność sieciową
    • Memory - pokazuje zużycie pamięci

Ten panel jest szczególnie przydatny do identyfikacji:

  • Długo działających skryptów blokujących główny wątek
  • Nadmiernych operacji na DOM
  • Problemów z wydajnością renderowania

3. Wbudowane API profilowania w React

React dostarcza wbudowane API do profilowania w trybie deweloperskim:

1// Importujemy komponent Profiler
2import { Profiler } from 'react';
3
4// Funkcja callback wywoływana po renderowaniu
5function onRenderCallback(
6  id, // identyfikator profilowanego drzewa komponentów
7  phase, // "mount" (pierwsze renderowanie) lub "update" (przerenderowanie)
8  actualDuration, // czas spędzony na renderowaniu
9  baseDuration, // szacowany czas renderowania całego poddrzewa bez optymalizacji
10  startTime, // kiedy React rozpoczął renderowanie
11  commitTime, // kiedy React zakończył renderowanie
12  interactions // zestaw "interakcji" śledzonych dla tego renderowania
13) {
14  console.log({
15    id,
16    phase,
17    actualDuration,
18    baseDuration,
19    startTime,
20    commitTime
21  });
22}
23
24// Użycie komponentu Profiler w aplikacji
25function App() {
26  return (
27    <Profiler id="MissionControl" onRender={onRenderCallback}>
28      <MissionControl />
29    </Profiler>
30  );
31}

Możesz zagnieżdżać komponenty Profiler, aby profilować różne części aplikacji:

1function Dashboard() {
2  return (
3    <Profiler id="Dashboard" onRender={onRenderCallback}>
4      <h1>Panel kontrolny misji</h1>
5      
6      <Profiler id="Navigation" onRender={onRenderCallback}>
7        <Navigation />
8      </Profiler>
9      
10      <Profiler id="Stats" onRender={onRenderCallback}>
11        <MissionStats />
12      </Profiler>
13      
14      <Profiler id="Crew" onRender={onRenderCallback}>
15        <CrewStatus />
16      </Profiler>
17    </Profiler>
18  );
19}

Ten sposób profilowania jest szczególnie przydatny, gdy:

  • Chcesz zbierać dane wydajnościowe w środowisku testowym
  • Masz specyficzne komponenty, które chcesz monitorować
  • Chcesz zautomatyzować zbieranie danych wydajnościowych

Typowe problemy wydajnościowe i ich profilowanie

1. Zbyt częste renderowanie komponentów

Problem: Komponenty renderują się ponownie, nawet gdy ich dane nie zmieniły się.

Jak profilować:

  • Użyj React DevTools Profiler, aby zobaczyć, które komponenty renderują się zbyt często
  • Sprawdź, czy komponenty rodzice nie powodują niepotrzebnych renderowań potomków

Przykład problemu:

1function ParentComponent() {
2  const [count, setCount] = useState(0);
3  
4  // Ta funkcja jest tworzona na nowo przy każdym renderowaniu
5  const handleClick = () => {
6    console.log('Button clicked');
7  };
8  
9  return (
10    <div>
11      <button onClick={() => setCount(count + 1)}>
12        Zwiększ licznik: {count}
13      </button>
14      
15      {/* ChildComponent będzie renderowany ponownie przy każdej zmianie count,
16          nawet jeśli handleClick ma taką samą funkcjonalność */}
17      <ChildComponent onClick={handleClick} />
18    </div>
19  );
20}

Rozwiązanie:

1function ParentComponent() {
2  const [count, setCount] = useState(0);
3  
4  // Memoizacja funkcji zapobiega tworzeniu nowej referencji przy każdym renderowaniu
5  const handleClick = useCallback(() => {
6    console.log('Button clicked');
7  }, []);
8  
9  return (
10    <div>
11      <button onClick={() => setCount(count + 1)}>
12        Zwiększ licznik: {count}
13      </button>
14      
15      {/* ChildComponent owinięty w React.memo nie renderuje się ponownie,
16          gdy jego propsy nie zmieniają się */}
17      <MemoizedChildComponent onClick={handleClick} />
18    </div>
19  );
20}
21
22const MemoizedChildComponent = React.memo(ChildComponent);

2. Ciężkie obliczenia w renderowaniu

Problem: Wykonywanie skomplikowanych obliczeń podczas renderowania powoduje opóźnienia w UI.

Jak profilować:

  • Obserwuj w React DevTools Profiler komponenty, które mają najdłuższy czas renderowania
  • Użyj Chrome Performance Panel, aby zobaczyć długie zadania JavaScript

Przykład problemu:

1function DataAnalytics({ data }) {
2  // Ciężkie obliczenia wykonywane przy każdym renderowaniu
3  const processedData = processLargeDataset(data);
4  const statistics = calculateStatistics(processedData);
5  
6  return (
7    <div>
8      <h2>Analiza danych misji</h2>
9      <DataVisualisation stats={statistics} />
10    </div>
11  );
12}

Rozwiązanie:

1function DataAnalytics({ data }) {
2  // Memoizacja ciężkich obliczeń - wykonują się tylko gdy data się zmienia
3  const processedData = useMemo(() => {
4    return processLargeDataset(data);
5  }, [data]);
6  
7  const statistics = useMemo(() => {
8    return calculateStatistics(processedData);
9  }, [processedData]);
10  
11  return (
12    <div>
13      <h2>Analiza danych misji</h2>
14      <DataVisualisation stats={statistics} />
15    </div>
16  );
17}

3. Nadmierne aktualizacje stanu

Problem: Zbyt częste aktualizacje stanu powodują kaskadowe rerenderowania.

Jak profilować:

  • W React DevTools Profiler znajdź komponenty oznaczone na czarno (inicjatorzy aktualizacji)
  • Obserwuj wzorce aktualizacji - czy niektóre komponenty aktualizują się w grupach?

Przykład problemu:

1function SpaceshipSensors() {
2  const [temperature, setTemperature] = useState(0);
3  const [pressure, setPressure] = useState(0);
4  const [oxygen, setOxygen] = useState(0);
5  const [radiation, setRadiation] = useState(0);
6  
7  // Pobieranie danych z czujników co 100ms
8  useEffect(() => {
9    const interval = setInterval(() => {
10      // Każda aktualizacja powoduje osobne renderowanie
11      setTemperature(getTemperature());
12      setPressure(getPressure());
13      setOxygen(getOxygen());
14      setRadiation(getRadiation());
15    }, 100);
16    
17    return () => clearInterval(interval);
18  }, []);
19  
20  return (
21    <div>
22      <h2>Odczyty czujników</h2>
23      <p>Temperatura: {temperature}°C</p>
24      <p>Ciśnienie: {pressure} hPa</p>
25      <p>Poziom tlenu: {oxygen}%</p>
26      <p>Poziom radiacji: {radiation} μSv/h</p>
27    </div>
28  );
29}

Rozwiązanie:

1function SpaceshipSensors() {
2  // Przechowywanie powiązanych danych razem w jednym obiekcie stanu
3  const [sensorData, setSensorData] = useState({
4    temperature: 0,
5    pressure: 0,
6    oxygen: 0,
7    radiation: 0
8  });
9  
10  useEffect(() => {
11    const interval = setInterval(() => {
12      // Jedna aktualizacja stanu zamiast czterech
13      setSensorData({
14        temperature: getTemperature(),
15        pressure: getPressure(),
16        oxygen: getOxygen(),
17        radiation: getRadiation()
18      });
19    }, 100);
20    
21    return () => clearInterval(interval);
22  }, []);
23  
24  return (
25    <div>
26      <h2>Odczyty czujników</h2>
27      <p>Temperatura: {sensorData.temperature}°C</p>
28      <p>Ciśnienie: {sensorData.pressure} hPa</p>
29      <p>Poziom tlenu: {sensorData.oxygen}%</p>
30      <p>Poziom radiacji: {sensorData.radiation} μSv/h</p>
31    </div>
32  );
33}

4. Duże listy i wizualizacje danych

Problem: Renderowanie długich list lub skomplikowanych wizualizacji może spowalniać interfejs.

Jak profilować:

  • Użyj React DevTools Profiler, aby zidentyfikować komponenty list, które długo się renderują
  • Sprawdź, jak często renderują się poszczególne elementy listy

Przykład użycia React DevTools do profilowania wydajności listy:

  1. Uruchom profilowanie
  2. Przewiń listę, aby zobaczyć, jak reaguje aplikacja
  3. Zatrzymaj profilowanie
  4. Sprawdź, które komponenty mają najdłuższy czas renderowania w wykresie rankingowym

Rozwiązanie:

  • Wirtualizacja list (React Window, React Virtualized)
  • Memoizacja elementów listy
  • Paginacja zamiast wyświetlania wszystkich elementów naraz

Zaawansowane techniki profilowania

1. User Timing API

Browser Performance API pozwala na tworzenie własnych znaczników czasowych:

1function ExpensiveComponent({ data }) {
2  useEffect(() => {
3    // Początek pomiaru
4    performance.mark('expensive-calculation-start');
5    
6    // Wykonanie kosztownych obliczeń
7    const result = performExpensiveCalculation(data);
8    
9    // Koniec pomiaru
10    performance.mark('expensive-calculation-end');
11    
12    // Utworzenie pomiaru między znacznikami
13    performance.measure(
14      'expensive-calculation',
15      'expensive-calculation-start',
16      'expensive-calculation-end'
17    );
18    
19    // Logowanie wyników
20    console.log(performance.getEntriesByName('expensive-calculation'));
21  }, [data]);
22  
23  //
24}

Te znaczniki będą widoczne w panelu Performance w Chrome DevTools.

2. Monitorowanie wydajności w środowisku produkcyjnym

Dla monitorowania wydajności w produkcji można użyć:

  • Web Vitals - biblioteka Google do pomiaru kluczowych wskaźników wydajności
  • New Relic, Datadog, Sentry - narzędzia APM (Application Performance Monitoring)
  • Własne rozwiązania korzystające z Performance API i Profiler API

Przykład zbierania danych z React Profiler i wysyłania ich na serwer:

1function onRenderCallback(id, phase, actualDuration, baseDuration, startTime, commitTime) {
2  // Zbieramy tylko dane z fazy commit i trwające dłużej niż 10ms
3  if (phase === 'update' && actualDuration > 10) {
4    sendToAnalyticsServer({
5      componentId: id,
6      renderTime: actualDuration,
7      timestamp: Date.now()
8    });
9  }
10}
11
12function App() {
13  return (
14    <Profiler id="App" onRender={onRenderCallback}>
15      <AppContent />
16    </Profiler>
17  );
18}

Studium przypadku: Profilowanie i optymalizacja aplikacji React

Przeanalizujmy proces profilowania i optymalizacji wydajności aplikacji React na przykładzie panelu kontrolnego misji kosmicznej:

Krok 1: Identyfikacja problemu

Użytkownicy zgłaszają, że panel kontrolny działa wolno, szczególnie podczas wyświetlania danych telemetrycznych i listy zdarzeń.

Krok 2: Profilowanie aplikacji

Używamy React DevTools Profiler, aby zidentyfikować źródło problemu:

  1. Nagrywamy sesję podczas normalnego użytkowania panelu
  2. Analizujemy wyniki w Profilerze
  3. Odkrywamy, że:
    • Komponent
      TelemetryChart
      renderuje się bardzo długo (>100ms)
    • Komponent
      EventsList
      renderuje się za każdym razem, gdy przychodzą nowe dane telemetryczne
    • Funkcja
      calculateTrends
      zajmuje dużo czasu procesora

Krok 3: Optymalizacja

Na podstawie wyników profilowania wprowadzamy następujące optymalizacje:

  1. Optymalizacja TelemetryChart:
1// Przed optymalizacją
2function TelemetryChart({ data }) {
3  // Ciężkie przetwarzanie danych przy każdym renderowaniu
4  const chartData = processChartData(data);
5  
6  return <LineChart data={chartData} />;
7}
8
9// Po optymalizacji
10const TelemetryChart = React.memo(function TelemetryChart({ data }) {
11  // Memoizacja przetwarzania danych
12  const chartData = useMemo(() => processChartData(data), [data]);
13  
14  return <LineChart data={chartData} />;
15});
  1. Optymalizacja EventsList:
1// Przed optymalizacją
2function EventsList({ events }) {
3  return (
4    <div>
5      {events.map(event => (
6        <EventItem key={event.id} event={event} />
7      ))}
8    </div>
9  );
10}
11
12// Po optymalizacji
13const EventsList = React.memo(function EventsList({ events }) {
14  return (
15    <div>
16      {events.map(event => (
17        <MemoizedEventItem key={event.id} event={event} />
18      ))}
19    </div>
20  );
21});
22
23const MemoizedEventItem = React.memo(EventItem);
  1. Przeniesienie ciężkich obliczeń do web workera:
1// Przed optymalizacją
2function MissionControlPanel({ telemetryData }) {
3  const trends = calculateTrends(telemetryData);  // Blokuje główny wątek
4  
5  return (
6    <div>
7      <TrendsDisplay trends={trends} />
8      {/* inne komponenty */}
9    </div>
10  );
11}
12
13// Po optymalizacji
14function MissionControlPanel({ telemetryData }) {
15  const [trends, setTrends] = useState(null);
16  
17  useEffect(() => {
18    if (!telemetryData) return;
19    
20    // Utworzenie web workera
21    const worker = new Worker('./trendsWorker.js');
22    
23    // Nasłuchiwanie odpowiedzi
24    worker.addEventListener('message', (event) => {
25      setTrends(event.data);
26      worker.terminate();
27    });
28    
29    // Wysłanie danych do przetworzenia
30    worker.postMessage(telemetryData);
31  }, [telemetryData]);
32  
33  return (
34    <div>
35      {trends ? <TrendsDisplay trends={trends} /> : <LoadingSpinner />}
36      {/* inne komponenty */}
37    </div>
38  );
39}

Krok 4: Weryfikacja optymalizacji

Po wprowadzeniu optymalizacji ponownie profilujemy aplikację:

  1. Czas renderowania
    TelemetryChart
    spadł z >100ms do ~15ms
  2. Komponent
    EventsList
    nie renderuje się już przy aktualizacjach danych telemetrycznych
  3. Główny wątek nie jest już blokowany przez
    calculateTrends

Dzięki tym optymalizacjom interfejs użytkownika działa płynnie, a panel kontrolny reaguje natychmiast na interakcje użytkownika.

Najlepsze praktyki profilowania

  1. Profiluj w środowisku podobnym do produkcyjnego

    • Uruchamiaj aplikację w trybie produkcyjnym (
      npm run build
      i serwuj zbudowane pliki)
    • Testuj na urządzeniach podobnych do tych używanych przez użytkowników
  2. Ustal punkty odniesienia

    • Zmierz wydajność przed i po optymalizacjach
    • Określ konkretne cele wydajnościowe (np. czas pierwszego renderowania < 2s)
  3. Profiluj regularnie

    • Włącz profilowanie do cyklu rozwoju aplikacji
    • Monitoruj wydajność po wprowadzeniu większych zmian
  4. Koncentruj się na percepcji użytkownika

    • Priorytetyzuj optymalizacje, które mają największy wpływ na odczuwalną wydajność
    • Pamiętaj o kluczowych metrykach Web Vitals (LCP, FID, CLS)
  5. Dokumentuj wyniki profilowania

    • Zapisuj dane wydajnościowe przed i po optymalizacjach
    • Udostępniaj wiedzę i techniki optymalizacji zespołowi

Podsumowanie

Profilowanie wydajności jest niezbędnym elementem tworzenia wysokiej jakości aplikacji React. Podobnie jak inżynierowie kosmiczni monitorują każdy aspekt działania statku, tak i my powinniśmy stale analizować wydajność naszych aplikacji, aby zapewnić użytkownikom płynne i responsywne doświadczenie.

Najważniejsze punkty do zapamiętania:

  1. Używaj odpowiednich narzędzi - React DevTools Profiler, Chrome Performance Panel
  2. Identyfikuj wąskie gardła - znajdź komponenty, które renderują się zbyt często lub zbyt długo
  3. Stosuj odpowiednie techniki optymalizacji - memoizacja, unikanie zbędnych renderowań, wirtualizacja list
  4. Mierz skuteczność optymalizacji - porównuj wydajność przed i po zmianach
  5. Optymalizuj z umiarem - pamiętaj, że przedwczesna optymalizacja może prowadzić do nadmiernie skomplikowanego kodu

Dzięki tym technikom i narzędziom profilowania, będziesz w stanie tworzyć wydajne aplikacje React, które zapewnią doskonałe doświadczenie użytkownikom, nawet w najbardziej skomplikowanych interfejsach kosmicznego panelu kontrolnego.

Przejdź do CodeWorlds