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.
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:
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.
Instalacja React DevTools
Uruchomienie profilowania:
Analiza wyników:
Chrome DevTools zawiera panel Performance, który pozwala na bardziej ogólne profilowanie wydajności strony:
Uruchomienie profilowania:
Analizuj wyniki:
Ten panel jest szczególnie przydatny do identyfikacji:
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:
Problem: Komponenty renderują się ponownie, nawet gdy ich dane nie zmieniły się.
Jak profilować:
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);Problem: Wykonywanie skomplikowanych obliczeń podczas renderowania powoduje opóźnienia w UI.
Jak profilować:
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}Problem: Zbyt częste aktualizacje stanu powodują kaskadowe rerenderowania.
Jak profilować:
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}Problem: Renderowanie długich list lub skomplikowanych wizualizacji może spowalniać interfejs.
Jak profilować:
Przykład użycia React DevTools do profilowania wydajności listy:
Rozwiązanie:
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.
Dla monitorowania wydajności w produkcji można użyć:
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}Przeanalizujmy proces profilowania i optymalizacji wydajności aplikacji React na przykładzie panelu kontrolnego misji kosmicznej:
Użytkownicy zgłaszają, że panel kontrolny działa wolno, szczególnie podczas wyświetlania danych telemetrycznych i listy zdarzeń.
Używamy React DevTools Profiler, aby zidentyfikować źródło problemu:
TelemetryChart renderuje się bardzo długo (>100ms)EventsList renderuje się za każdym razem, gdy przychodzą nowe dane telemetrycznecalculateTrends zajmuje dużo czasu procesoraNa podstawie wyników profilowania wprowadzamy następujące optymalizacje:
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// 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// 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}Po wprowadzeniu optymalizacji ponownie profilujemy aplikację:
TelemetryChart spadł z >100ms do ~15msEventsList nie renderuje się już przy aktualizacjach danych telemetrycznychcalculateTrendsDzięki tym optymalizacjom interfejs użytkownika działa płynnie, a panel kontrolny reaguje natychmiast na interakcje użytkownika.
Profiluj w środowisku podobnym do produkcyjnego
npm run build i serwuj zbudowane pliki)Ustal punkty odniesienia
Profiluj regularnie
Koncentruj się na percepcji użytkownika
Dokumentuj wyniki profilowania
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:
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.