Spis treści
Wprowadzenie
Podczas pracy zawodowej zdarzało mi się doświadczać sytuacji, gdy pracując pod presją czasu trzeba było iść na “kompromisy” jeśli chodzi o jakość kodu. Z czasem w projekcie używającym Reacta pojawiały się komponenty typu “wielotysięczniki”, które miały setki a czasem nawet tysiące linii kodu. Im dłużej trwał development, tym więcej kodu dolepiane było do takich komponentów oraz rosła ich liczba zależności.
Gdy zaczęliśmy przerabiać takie olbrzymy na mniejsze komponenty, nieco zaskakującą korzyścią był spadek ilości rerenderów. Po zastanowieniu się, okazało się to proste i oczywiste – zarządzanie stanem, również przeniesione do różnych komponentów, powodowało rerendery tylko tych komponentów w których było zawarte.
Przykład w React
Sposób liczenia rerenderów
Choć istnieją rozwiązania jak np react-render-counter na potrzeby uproszczenia przykładu zastosujemy zwykłe zmienne:
let reportRerenderBeforeRefactorCounter = 0; let messageRerenderBeforeRefactorCounter = 0; let reportRerenderAfterRefactorCounter = 0; let messageRerenderAfterRefactorCounter = 0;
Wartości te inkrementowane będą za każdym razem gdy nastąpi wyrenderowanie elementu:
// wersja po refaktorze Report component rerenders: {reportRerenderAfterRefactorCounter} //... Message component rerender: {messageRerenderAfterRefactorCounter} // wersja przed refaktorem Report component rerenders: {reportRerenderBeforeRefactorCounter} //... Message component rerender: {messageRerenderBeforeRefactorCounter}
Kod przez refaktoryzacją
Spójrzmy najpierw na przykład aplikacji przed refaktoryzacją, gdzie wiele rzeczy dzieje się w jednym komponencie:
export const BeforeRefactorApp = () => { const [reportCount, setReportCount] = useState(0); const [message, setMessage] = useState(''); reportRerenderBeforeRefactorCounter++; messageRerenderBeforeRefactorCounter++; return ( <div style={{paddlingLeft: 20}}> <h1> Version before refactor </h1> <> <h2> Report component </h2> Report component rerenders: {reportRerenderBeforeRefactorCounter} <br /> <button onClick={() => setReportCount(state => state + 1)}> Send "report" :) Report count: {reportCount} </button> </> <> <h2> Form component </h2> <form onSubmit={e => { e.preventDefault(); setMessage(''); alert(`Sent message: ${message}`); }}> Message component rerender: {messageRerenderBeforeRefactorCounter} <br /> <input value={message} onChange={e => setMessage(e.target.value)}/> <br /> <button type="submit"> Send message </button> </form> </> </> ); }
W takiej wersji, ponieważ w jednym komponencie znajdują się wszystkie elementy zmieniające stan aplikacji, rerenderują się również te elementy co do których nie ma potrzeby by to robić:
- Klikanie przycisku “raportującego” rerenderuje formularz
- Wpisywanie tekstu w pole rerenderuje przycisk
Stąd poza ogólnym nieładem, aplikacja może nie być zbyt wydajna -liczba rerenderów obu elementów jest taka sama:
Kod po refaktoryzacji
Kiedy jednak wydzielimy kod elementów do osobnych komponentów:
export const AfterRefactorApp = () => { return ( <> <h2>Version after refactor</h2> <ReportComponent /> <MessageComponent /> </> ) }
const ReportComponent = () => { const [reportCount, setReportCount] = useState(0); reportRerenderAfterRefactorCounter++; return ( <> <h3> Report component </h3> Report component rerenders: {reportRerenderAfterRefactorCounter} <br /> <button onClick={() => setReportCount(state => state + 1)}> Send "report" :) Report count: {reportCount} </button> </> ) }
const MessageComponent = () => { const [message, setMessage] = useState(''); messageRerenderAfterRefactorCounter++; return ( <> <h3> Form component </h3> <form onSubmit={e => { e.preventDefault(); setMessage(''); alert(`Sent message: ${message}`); }}> Message component rerender: {messageRerenderAfterRefactorCounter} <br /> <input value={message} onChange={e => setMessage(e.target.value)}/> <br /> <button type="submit"> Send message </button> </form> </> ); }
Każdy komponent będzie sam zarządzał swoim stanem. Dzięki temu zmiany w jednym komponencie nie będą wpływać na rerenderowanie drugiego. Spójrzmy jak przedstawia się orientacyjnie liczba rerenderów:
Wartości są różne, ponieważ każdy komponent “liczy” tylko swoje rerendery. Stąd rozbieżność – mimo że w komponencie formularza zostało wpisanych wiele znaków, żaden z rerenderów nie wpłynął na komponent z przyciskiem raportów.
Kod przykładu na Github
Pełny kod przykładu znajdziesz pod adresem:
https://github.com/radek-anuszewski/react-minimize-rerenders-demo
Klikany przykład na Github Pages
Klikalny przykład znajdziesz pod adresem:
https://radek-anuszewski.github.io/react-minimize-rerenders-demo/
Podsumowanie
Gdy myślimy o refaktoryzacji – wydzielaniu komponentów na mniejsze, rozdzielaniu zakresów odpowiedzialności – rzadko pojawia się temat wydajności aplikacji. Okazuje się jednak, że jeśli rozbijemy duże komponenty na mniejsze, poprawnie poukładamy co do którego należy to darmowym zyskiem będzie poprawa wydajności naszej aplikacji – dlatego zanim zastosujesz optymalizacje typu React.memo sprawdź, czy przypadkiem małe wyczyszczenie kodu nie załatwi tego problemu przy okazji.