Sprawdź, jak prosty refaktor może zredukować liczbę rerenderów w Twojej aplikacji!

Spis treści

  1. Wprowadzenie
  2. Przykład w React
    1. Sposób liczenia rerenderów
    2. Kod przez refaktoryzacją
    3. Kod po refaktoryzacji
    4. Kod przykładu na Github
    5. Klikany przykład na Github Pages
  3. Podsumowanie

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ć:

  1. Klikanie przycisku “raportującego” rerenderuje formularz
  2. 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.