Co optymalizuje kompilator React i co pozostawia na twoim talerzu
Zrozum, jakie zadania związane z wydajnością automatyzuje React Compiler, dlaczego powolne API i duże pliki pakietów pozostają twoją odpowiedzialnością, oraz jak bezpiecznie wdrożyć go w istniejącej bazie kodu React.
React Compiler eliminuje dużą część ręcznej memoizacji, co sprawia, że łatwo jest traktować go jako rozwiązanie na powolne aplikacje. Tak nie jest – optymalizuje jedynie proces renderowania kontrolowany przez React, i nic więcej. Poniżej zobaczysz, gdzie leży ta granica, jakie spowolnienia nadal wymagają interwencji inżyniera oraz jak włączyć kompilator w istniejącej bazie kodu bez powodowania regresji.
Od listy kontrolnej memoizacji do automatycznej memoizacji
Przez długi czas porady dotyczące wydajności React sprowadzały się do krótkiej listy narzędzi:
useMemo()
useCallback()
React.memo()
Komponent renderował się zbyt często, więc go otoczyłeś. Obliczenie wydawało się kosztowne, więc je skompilowałeś w pamięci. Funkcja zwrotna była wysyłana do potomka, więc ją ustabilizowałeś. Każde rozwiązanie polegało na ręcznej memoizacji, przy użyciu tablicy zależności do jej utrzymania.
React Compiler zmienia tę domyślną praktykę. Wersja 1.0 została uznana za stabilną, a zaleceniem zespołu React dotyczącym nowego kodu jest pozostawienie kompilatorowi zadania memoizacji zamiast ręcznego stosowania useMemo i useCallback. Sprawdź aktualne dokumentacje React w celu uzyskania najnowszych porad dotyczących ich stosowania.
Kluczowa kwestia można streścić w jednym zdaniu: kompilator może sprawić, że praca wykonywana przez sam React będzie tańsza, ale nie ma wpływu na operacje odbywające się poza Reactem.
Czym właściwie zajmuje się kompilator
Podczas budowania aplikacji kompilator analizuje komponenty i hooki oraz wstawia mechanizmy memoizacji tam, gdzie jego analiza pokazuje, że jest to korzystne. Weźmy na przykład komponent, który filtrował listę produktów przed jej wyświetleniem:
function ProductList({ products }) {
const visible = products.filter(
product => product.inStock
);
Wtedy z filtrowaną listą przechodzi się bezpośrednio do komponentu potomnego:
return <List items={visible} />;
}
Zanim pojawi się kompilator, programista skupiony na wydajności często wyraźnie cacheuje filtr, aby List otrzymywał tę samą referencję do tablicy aż do zmiany wartości products:
const visible = useMemo(
() => products.filter(
product => product.inStock
),
[products]
);
Gdy kompilator jest włączony, zazwyczaj pomija się taki kod. Sam określa zależności, może zapamiętywać je dokładniej niż ręcznie napisane hooki, i nie polega na tym, że poprawnie wymienisz zależności. React nazywa to automatyczną memoizacją komponentów i hooki.
Zwróć jednak uwagę na zakres opisu – każdy element listy to konstrukcja Reacta.
Model mentalny: optymalizator renderowania
Pomaga wyobrazić sobie kompilator jako coś, co działa nad dwoma rodzajami zadań w React i je optymalizuje:
React Compiler
│
┌───────────┴───────────┐
▼ ▼
Component work Hook calculations
│ │
▼ ▼
memoization memoization
│ │
└───────────┬───────────┘
▼
fewer unnecessary
React updates
Mniej niepotrzebnych ponownych renderowań może być prawdziwym atutem. Najważniejszą częścią diagramu jest jednak to, co on pominął.
Powolne backendy są poza jego zasięgiem
Rozważmy komponent asynchroniczny, który czeka na odpowiedź z API przed renderowaniem panelu sterowania:
async function Dashboard() {
const data = await fetch(
"https://api.example.com/dashboard"
);
Gdy dane przyjdą, są przekazywane do interfejsu użytkownika:
return <DashboardUI data={data} />;
}
Jeśli ten endpoint potrzebuje 800 ms na przetworzenie, kompilator nie skróci tego czasu do 50 ms. Może jedynie uproszczyć proces przekształcania danych w interfejs użytkownika w React, ale nie sprawi, że backend będzie odpowiadał szybciej. W formie wykresu czasowego:
Browser
│
▼
React
│
▼
API request ──────────► 800ms
│
▼
Response
│
▼
React render
Memoizacja występuje niemal wyłącznie na ostatnim etapie. Jeśli większość czasu jest poświęcona oczekiwaniu na połączenie sieciowe, optymalizujesz niewłaściwą warstwę; rozwiązania to szybsze zapytania, cacheowanie, mniejsze odpowiedzi lub pobieranie danych równolegle. Aby oddzielić czas połączenia sieciowego od czasu działania klienta, zobacz Diagnozowanie problemów wydajności React poza czasem odpowiedzi API.
JavaScript, którego nie powinnoś był używać
To samo ograniczenie dotyczy rozmiaru pliku bundle. Wyobraź sobie stronę startową, która ładuje ciężką bibliotekę do tworzenia wykresów, edytor tekstu z funkcjami formatowania oraz SDK analityczne, zanim ktokolwiek otworzy ekrany, które je wykorzystują:
Initial page
│
├── React
├── UI
├── Chart library
├── Editor
└── Analytics SDK
Kompilator nie usuwa żadnych zależności; każdy bajt nadal jest pobierany, analizowany i wykonywany. Pomaga podział kodu, na przykład ładowanie ekranu analitycznego na żądanie:
const Analytics = lazy(
() => import("./Analytics")
);
Teraz kod jest pobierany tylko wtedy, gdy funkcja jest faktycznie renderowana, a otaczająca go granica Suspense pokazuje rozwiązanie awaryjne podczas ładowania. To decyzja związana z architekturą bundle’ów, i żadna forma memoizacji nie może jej zastąpić.
Gdzie pojawiają się błędy w oczekiwaniach
Częsty scenariusz: aplikacja działa wolno, ktoś włącza kompilator, a zespół oczekuje takiego wyniku:
Slow app
│
▼
React Compiler
│
▼
Fast app
To zbyt proste. Wolność ma kilka przyczyn, a kompilator obejmuje tylko jedną z nich:
SLOW APP
│
┌───────────────┼───────────────┐
▼ ▼ ▼
React work Network Browser
│ │ │
▼ ▼ ▼
Compiler API/cache JS/rendering
│ │ │
▼ ▼ ▼
Optimized Engineer Engineer
Kompilator to tylko jeden narzędzie w zestawie do poprawy wydajności, a nie sam zestaw narzędzi. Opóźnienia sieciowe oraz koszty przeglądarki, takie jak wykonywanie skryptów i układanie elementów, nadal wymagają interwencji człowieka.
Czy należy usunąć istniejące wywołania useMemo?
Nie automatycznie. W nowym kodzie oficjalną domyślną praktyką jest pozwalanie kompilatorowi na tworzenie memoizacji. W przypadku istniejącego kodu React zaleca pozostawienie obecnej memoizacji bez zmian lub jej usunięcie dopiero po dokładnych testach. Powodem jest aspekt behawioralny, a nie tylko wydajność: wartość zmemoizowana użyta jako zależność Effect określa, kiedy ten Effect zostanie ponownie uruchomiony, więc zmiana sposobu jej memoizacji może wpłynąć na moment wywołania efektów ubocznych.
Dlatego opieraj się pokusie przeprowadzenia takiej czyszczenia całego kodu:
useMemo → DELETE
useCallback → DELETE
React.memo → DELETE
Bezpieczniejszą drogą jest postępowanie stopniowe, z pomiarami na każdym kroku:
Existing app
│
▼
Enable compiler
│
▼
Run tests
│
▼
Profile
│
▼
Remove unnecessary
manual memoization
carefully
Kompilator ma na celu zmniejszenie konieczności ręcznej memoizacji w przyszłości, a nie natychmiastowe uznanie każdej istniejącej optymalizacji za błędną.
Kompilator jako narzędzie kontroli jakości kodu
Korzyść, która przyciąga mniej uwagi: kompilator zawiera kroki walidacji oparte na zasadach React, a jego integracja z narzędziem linting może wykrywać takie wzorce:
set-state-in-render
set-state-in-effect
unsafe ref access
React zaznacza, że te diagnozy mogą ujawnić błędy już obecne w aplikacji. Ponieważ kompilator pomija kod, który narusza jego założenia, ostrzeżenia pokazują również, które komponenty nie zostały zoptymalizowane, co czyni je częścią codziennego cyklu informacyjnego, a nie tylko dodatkiem do poprawy wydajności.
Najpierw sprawdź profil, potem wybierz rozwiązanie
Gdy interfejs wydaje się wolny, zmierz jego wydajność przed użyciem jakiegokolwiek narzędzia i pozwól wynikom pomocy w wyborze rozwiązania:
SLOW UI
│
▼
PROFILE
│
┌────────┼────────┐
▼ ▼ ▼
React Network Browser
work │ │
│ │ │
▼ ▼ ▼
Compiler API/cache JS/render
│
▼
Targeted fix
Następnie dopasuj rozwiązanie do tego, co pokazuje narzędzie do analizy profilu:
- Niepotrzebne ponowne renderowanie komponentów React: tu może pomóc Kompilator React.
- Wolna API: ulepsz punkt końcowy lub strategię pobierania danych.
Główne wnioski
Stary model wymagał podawania Reactowi informacji o tym, co należy zapamiętać; nowy pozwala pisać poprawny kod w React i polegać na kompilatorze przy większości zadań renderowania. To podnosi wartość jednej umiejętności: znajomości tego, gdzie faktycznie traci się czas.
- Kompilator optymalizuje te zadania, które rozumie, a są to renderowanie w React i obliczenia hooków.
- Nie może on zmniejszyć rozmiaru zbyt dużej odpowiedzi API, przyspieszyć zapytanie do bazy danych, usunąć skrypt od third-party ani skompresować obrazu o wielkości 5 MB.
- Nie może też uratować architektury, która wszędzie generuje niepotrzebną pracę.
- W istniejącym kodzie włącz tę funkcję, przeprowadź testy i analizę profilową, a dopiero potem usuń ręczną memoizację.
- Rozpatruj jej dane diagnostyczne jako bezpłatne zgłoszenia błędów dotyczące naruszeń zasad React.
Nie pytaj, czy kompilator jest włączony, ale gdzie aplikacja traci czas; to właśnie na to służy kompilator – aby zmniejszyć udział React w tym czasie.
Literatura pokrewna
- Poza rozmiarem bundle: odkrywanie tego, co naprawdę spowalnia twoją aplikację webową — Dlaczego redukcja kilobajtów rzadko naprawia wolną aplikację oraz jak śledzić rzeczywisty czas oczekiwania na serwerach, w procesach typu waterfall, podczas hydratacji oraz w skryptach i obrazach od dostawców zewnętrznych.
- Jak inżynieria frontendu ewoluowała od stylizacji do systemów skalowalnych — Opisuje przejście od podstawowych technik HTML/CSS/JS do architektury komponentów, cache’owania, monorepo oraz mechanizmów obserwowalności niezbędnych do niezawodnego obsługi milionów użytkowników.
- Rób mniej pracy najpierw: lista kontrolna wydajności React przed użyciem memoizacji — Zmniejsz koszty aplikacji React, unikając niepotrzebnych działań: używaj debounce do zapytań, paginuj treść, umieszczaj stan w odpowiednich miejscach, stosuj stabilne klucze, przenoś obliczenia na stronę serwera i używaj ładowania opóźnionego, a dopiero potem zastosuj memoizację w razie potrzeby.