Studium przypadku optymalizacji wydajności SPA w React w rzeczywistych warunkach
Wydajność witryny to nie tylko czas jej wczytywania. Ważne jest, aby zapewnić użytkownikom szybkie i wygodne działanie aplikacji, zwłaszcza w przypadku aplikacji biurowych, których użytkownicy używają codziennie. Zespół inżynierów w firmie Recruit Technologies przeprowadził projekt refaktoryzacji, aby ulepszyć jedną ze swoich aplikacji internetowych, AirSHIFT, pod kątem wydajności wprowadzania danych przez użytkowników. Oto jak to zrobili.
Powolna reakcja, mniejsza produktywność
AirSHIFT to aplikacja internetowa na komputer, która pomaga właścicielom sklepów, np. restauracji i kawiarni, zarządzać pracą zmianową pracowników. Ta aplikacja jednostronicowa została stworzona w React i zawiera bogate funkcje klienta, w tym różne tabele siatki harmonogramów zmian uporządkowane według dnia, tygodnia, miesiąca itp.
Gdy zespół inżynierów Recruit Technologies dodawał nowe funkcje do aplikacji AirSHIFT, zaczął otrzymywać więcej opinii dotyczących jej niskiej wydajności. Yosuke Furukawa, kierownik ds. inżynierii AirSHIFT, powiedział:
W ramach badań opinii użytkowników byliśmy zdumieni, gdy jeden z właścicieli sklepów powiedział, że po kliknięciu przycisku schodzi ze swojego miejsca, aby zaparzyć kawę, bo chce zabić czas oczekiwania na załadowanie tabeli zmian.
Po przeanalizowaniu wyników zespół inżynierów doszedł do wniosku, że wielu użytkowników próbuje wczytywać ogromne tabele na komputerach o niskich specyfikacjach, takich jak laptop Celeron M o taktowaniu 1 GHz sprzed 10 lat.
Aplikacja AirSHIFT blokowała główny wątek za pomocą drogich skryptów, ale zespół inżynierów nie zdawał sobie sprawy, jak drogie są te skrypty, ponieważ były one opracowywane i testowane na zaawansowanych komputerach z szybkimi połączeniami Wi-Fi.
Po przeprowadzeniu profilowania wydajności w Narzędziech deweloperskich Chrome z włączonym ograniczeniem procesora i ograniczeniem przepustowości sieci okazało się, że konieczna jest optymalizacja wydajności. Do rozwiązania tego problemu powstała grupa zadaniowa AirSHIFT. Oto 5 rzeczy, na których skupili się, aby ich aplikacja była bardziej responsywna na działania użytkowników.
1. Wirtualizacja dużych tabel
Wyświetlanie tabeli zmian wymagało wykonania wielu kosztownych kroków: tworzenia wirtualnego DOM-u i renderowania go na ekranie proporcjonalnie do liczby pracowników i slotów czasowych. Jeśli np. restauracja zatrudnia 50 pracowników i chce sprawdzić ich miesięczny harmonogram zmian, będzie to tabela z 50 osobami (pracownikami) pomnożona przez 30 dni, co da 1500 komponentów komórek do renderowania. Jest to bardzo kosztowna operacja, zwłaszcza w przypadku urządzeń o niskich specyfikacjach. W rzeczywistości było jeszcze gorzej. Okazało się, że istnieją sklepy, w których zatrudnia się 200 pracowników, a w jednej tabeli miesięcznej znajduje się około 6000 komórek.
Aby zmniejszyć koszty tej operacji, AirSHIFT zwirtualizował tabelę zmian. Aplikacja podłącza teraz tylko komponenty znajdujące się w widocznym obszarze i odłącza te spoza ekranu.
W tym przypadku firma AirSHIFT używała react-virtualized, ponieważ wymagała to włączenia złożonych dwuwymiarowych tabel siatki. Badamy też możliwości przekształcenia implementacji, aby w przyszłości używać lekkiego pakietu react-window.
Wyniki
Sam proces wirtualizacji tabeli skrócił czas tworzenia skryptu o 6 sekund (przy 4-krotnym spowolnieniu procesora i szybkim ograniczeniu szybkości Macbooka Pro do sieci 3G). Było to najbardziej znaczące polepszenie wydajności w ramach projektu refaktoryzacji.
2. Kontrola za pomocą interfejsu User Timing API
Następnie zespół AirSHIFT przeprowadził refaktoryzację skryptów, które działają na podstawie danych wejściowych użytkownika. Grafika klepsydry w Narzędziach deweloperskich w Chrome umożliwia analizę tego, co dzieje się na głównym wątku. Zespół AirSHIFT stwierdził jednak, że łatwiej jest analizować aktywność aplikacji na podstawie cyklu życia React.
React 16 udostępnia dane śledzenia wydajności za pomocą interfejsu User Timing API, który możesz zwizualizować w sekcji Czasy w Narzędziach deweloperskich w Chrome. Firma AirSHIFT korzystała z sekcji Czasy trwania, aby znaleźć niepotrzebną logikę działającą w zdarzeniach cyklu życia React.
Wyniki
Zespół AirSHIFT odkrył, że przed każdą nawigacją trasy występuje niepotrzebne pojednanie drzewa React. Oznacza to, że React niepotrzebnie aktualizował tabelę shift przed nawigacją. Problem powodował niepotrzebny update stanu Redux. Dzięki temu udało się skrócić czas skryptu o około 750 ms. AirSHIFT wprowadził też inne mikrooptymalizacje, które ostatecznie doprowadziły do skrócenia czasu tworzenia skryptu o 1 sekundę.
3. Leniwe ładowanie komponentów i przenoszenie kosztownej logiki do web workerów
AirSHIFT ma wbudowaną aplikację do czatu. Wielu właścicieli sklepów komunikuje się ze swoimi pracownikami na czacie, jednocześnie korzystając z tabeli zmian. Oznacza to, że użytkownik może wpisywać wiadomość, gdy tabela jest wczytywana. Jeśli wątek główny jest zajęty przez skrypty renderujące tabelę, dane wejściowe użytkownika mogą być niesprawne.
Aby ulepszyć ten proces, AirSHIFT korzysta teraz z React.lazy i Suspense, aby wyświetlać elementy zastępcze dla zawartości tabeli, a jednocześnie wczytywać komponenty w sposób opóźniony.
Zespół AirSHIFT przeniósł też część kosztownych logiki biznesowej w komponentach ładowanych z leniwym ładowaniem do instancji roboczych. Rozwiązało to problem z zawieszaniem się aplikacji podczas wprowadzania danych przez użytkownika, ponieważ zwolnił to główny wątek, który mógł się skupić na reagowaniu na dane wejściowe użytkownika.
Zwykle deweloperzy mają problemy z korzystaniem z robotów, ale tym razem Comlink wykonał za nich ciężką pracę. Poniżej znajdziesz pseudo kod pokazujący, jak firma AirSHIFT przeprowadziła jedną z najdroższych operacji, czyli obliczanie łącznych kosztów pracy.
W App.js użyj React.lazy i Podejrzenia, aby wyświetlać treści zastępcze podczas wczytywania
/** App.js */
import React, { lazy, Suspense } from 'react'
// Lazily loading the Cost component with React.lazy
const Hello = lazy(() => import('./Cost'))
const Loading = () => (
<div>Some fallback content to show while loading</div>
)
// Showing the fallback content while loading the Cost component by Suspense
export default function App({ userInfo }) {
return (
<div>
<Suspense fallback={<Loading />}>
<Cost />
</Suspense>
</div>
)
}
W komponencie Koszt użyj polecenia comlink, aby wykonać logikę obliczeniową
/** Cost.js */
import React from 'react';
import { proxy } from 'comlink';
// import the workerlized calc function with comlink
const WorkerlizedCostCalc = proxy(new Worker('./WorkerlizedCostCalc.js'));
export default async function Cost({ userInfo }) {
// execute the calculation in the worker
const instance = await new WorkerlizedCostCalc();
const cost = await instance.calc(userInfo);
return <p>{cost}</p>;
}
Wdróż logikę obliczeń, która działa w procesie roboczym, i ujawnij ją za pomocą funkcji comlink
// WorkerlizedCostCalc.js
import { expose } from 'comlink'
import { someExpensiveCalculation } from './CostCalc.js'
// Expose the new workerlized calc function with comlink
expose({
calc(userInfo) {
// run existing (expensive) function in the worker
return someExpensiveCalculation(userInfo);
}
}, self);
Wyniki
Pomimo ograniczonej ilości logiki przeniesionej do instancji roboczej w ramach testów, AirSHIFT przeniósł około 100 ms kodu JavaScript z głównego wątku do wątku roboczego (symulowanego z 4-krotnym ograniczeniem procesora).
Obecnie AirSHIFT sprawdza, czy może leniwie ładować inne komponenty i odciążać instancje logiczne, aby jeszcze bardziej ograniczyć ilość bałaganu.
4. Ustawianie budżetu wydajności
Po wdrożeniu wszystkich tych optymalizacji należało zadbać o to, aby aplikacja nadal działała sprawnie. AirSHIFT używa teraz parametru bundlesize, aby nie przekraczać bieżącego rozmiaru plików JavaScript i CSS. Oprócz ustawienia tych podstawowych budżetów zespół stworzył panel, który pokazuje różne wartości odsetka czasu ładowania tabeli zmian, aby sprawdzić, czy aplikacja działa prawidłowo nawet w niekorzystnych warunkach.
- Czas zakończenia skryptu w przypadku każdego zdarzenia Redux jest teraz mierzony
- Dane o wydajności są zbierane w Elasticsearch
- Skuteczność każdego zdarzenia w 10, 25, 50 i 75 centylu można przedstawić za pomocą narzędzia Kibana.
AirSHIFT monitoruje teraz zdarzenie wczytywania tabeli zmian, aby mieć pewność, że kończy się ono w ciągu 3 sekund w przypadku użytkowników w 75. percentylu. Na razie jest to budżet nieobowiązkowy, ale rozważamy automatyczne powiadomienia za pomocą Elasticsearch, gdy przekroczymy budżet.
Wyniki
Z wykresu powyżej można wywnioskować, że AirSHIFT osiąga obecnie budżet 3 sekund w przypadku użytkowników z 75. percentyla oraz wczytuje tabelę zmian w ciągu sekundy w przypadku użytkowników z 25. percentyla. Dzięki rejestrowaniu danych o wydajności RUM w różnych warunkach i na różnych urządzeniach AirSHIFT może teraz sprawdzać, czy nowa wersja funkcji rzeczywiście wpływa na wydajność aplikacji.
5. Hackathony dotyczące wydajności
Mimo że wszystkie te działania związane z optymalizacją wydajności były ważne i miały duży wpływ, nie zawsze łatwo jest przekonać zespoły inżynierów i biznesowe do nadania priorytetów rozwojowi niefunkcjonalnemu. Część problemu polega na tym, że niektórych optymalizacji skuteczności nie można zaplanować. Wymagają eksperymentowania i testowania metodą prób i błędów.
Zespół AirSHIFT prowadzi obecnie wewnętrzne jednodniowe hackathony dotyczące wydajności, aby umożliwić inżynierom skupienie się wyłącznie na pracy związanej z wydajnością. Podczas tych hackathonów nie ma żadnych ograniczeń i szanujemy kreatywność inżynierów, co oznacza, że warto rozważyć każde wdrożenie, które przyspiesza działanie. Aby przyspieszyć hackathon, AirSHIFT dzieli grupę na małe zespoły, które rywalizują ze sobą o to, kto uzyska największą poprawę wyniku w Lighthouse. Drużyny stają się bardzo konkurencyjne. 🔥
Wyniki
Hackathony sprawdzają się w ich przypadku.
- Wszelkie wąskie gardła w zakresie wydajności można łatwo wykryć, testując podczas hackathonu różne podejścia i mierząc je za pomocą Lighthouse.
- Po hackathonie łatwo przekonać zespół, której optymalizacji należy nadać priorytet w przypadku wersji produkcyjnej.
- Jest to też skuteczny sposób na zachęcanie do szybszego działania. Każdy uczestnik rozumie zależność między sposobem tworzenia kodu a jego wpływem na wydajność.
Miłym skutkiem ubocznym było to, że wiele innych zespołów programistycznych w Recruit zainteresowało się tym praktycznym podejściem, a zespół AirSHIFT prowadzi obecnie w firmie wiele hackathonów.
Podsumowanie
Praca nad tymi optymalizacjami nie była łatwa, ale opłaciła się. Teraz aplikacja AirSHIFT wczytuje tabelę zmian w średnim czasie 1,5 sekundy, co oznacza 6-krotne przyspieszenie w porównaniu z czasem wczytywania przed rozpoczęciem projektu.
Po wprowadzeniu optymalizacji skuteczności jeden z użytkowników powiedział:
Dziękujemy za szybkie wczytanie tabeli zmian. Organizowanie pracy zmianowej jest teraz znacznie bardziej wydajne.