Strona główna / Artykuły

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.

Wydawca: REACTAPP.TOP6 min czytania1421 słów

Źródła: React Compiler Won’t Fix a Slow React App

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.
  • Zbyt duża ilość kodu: przesyłaj mniej JavaScript poprzez dzielenie kodu i redukcję zależności.
  • Drogie aktualizacje DOM: przemyśl sposób renderowania, na przykład poprzez wirtualizację długich list.
  • Ciężki skrypt od third-party: usuń go, odłóż jego wykonywanie lub izoluj go.
  • 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