• Post last modified:06/04/2020
  • Post Category:React
  • Post Comments:0 Komentarzy

Kolejny wpis po monitorowaniu pracy dotyczący poprawy wydajności aplikacji pisanych w React. Tym razem skupimy się na jednym specyficznym zagadnieniu jakim jest Code Splitting.

Większość z nas podczas tworzenia aplikacji w React korzysta z jakiegoś bundlera, który łączy wszystkie pliki naszego projektu w jeden zminimalizowany i zoptmalizowany produkcyjnie plik, tzw. bundle. Osoby korzystające z create-react-appNext.js czy GatsbyJS będą miały z automatu dołączonego do nowo tworzonych projektów Webpacka. Część osób zaczynających z aplikacją Reactową od zera być może skusi się na Parcel.js. W każdym z tych przypadków domyślne ustawienia bundlera zazwyczaj będzie nam budowało jeden wynikowy plik .js z naszą całą aplikacją.

Code Splitting

Tworzenie jednego pliku jest w zupełności wystarczające gdy nasza aplikacja jest dość mała i nie zawiera dużych zewnętrzynych bibliotek. W przypadku bardziej złożonych aplikacji takie podejście może powodować problemy wydajnościowe. Załóżmy sytuację gdy w naszym programie mamy kilka ekranów zawierających duże wykresy bądź ciężkie edytory tekstowe. W podejściu z jednym plikiem wszystkie te komponenty zostaną załadowane do pamięci przeglądarki naszego użytkownika mimo, iż być może nigdy on nie skorzysta z w/w ekranów.

Najlepszym rozwiązaniem w tej sytuacji byłoby wydzielenie niektórych elementów z naszego zbiorczego pliku. Następnie możemy ładować je tylko wtedy gdy rzeczywiście będą potrzebne. Zredukujemy w ten sposób również rozmiar pliku wykonywanego podczas pierwszego ładowania aplikacji. Finalnie więc dostarczymy użytkownikowi pierwsze elementy na ekranie znacznie szybciej.

W React mamy kilka sposób na to, aby zaimplementować Code Splitting i przyjrzymy się dzisiaj najpopularniejszym z nich.

Dynamic imports

W obecnej wersji specyfikacji ECMAScript komponenty eksportowane jako ES modules są kompletnie statyczne. Muszą być eksportowane i importowane jedynie podczas kompilowania, nie podczas wykonywania programu. Dlatego też importy w naszych komponentach są zawsze umieszczane na samym początku pliku. Nie ma możliwości importowania ich dynamicznie, np. w wyrażeniu switch {}

Na nasze szczęście taka funkcjonalność jest już dostępna jako eksperymentalny feature i znajduje się w fazie Stage 3 w procesie dodawania nowych funkcjonalności do standardu ECMAScript. Prawdopodobnie już niedługo będzie powszechnie dostępna. W chwili obecnej musimy jeszcze wspierać się Babelem oraz Webpackiem aby móc skorzystać z dynamicznych importów.

Wszystkie nowe aplikacje korzystające z create-react-app bądź Next.js mają tą funkcjonalnść skonfigurowaną domyślnie. Wszyscy inni powinni skorzystać z instrukcji dla Webpacka oraz skorzystać z odpowiedniego pluginu dla Babela.

Dynamiczny import możemy użyć w każdym miejscu naszej aplikacji. Różni się on od tradycyjnego importu tym, iż import wywołany w ten sposób zwraca nam Promise:

if (newMail === true) {
  import('./BigTextEditor')
    .then(module => module.showEditor())
    .catch(e => throw new Error(e) )
}

// or async/await
let module = await import('./BigTextEditor');

Myślę, że użycie dynamicznego importu jest dość proste i nie wymaga większych wyjaśnień. Należy jednak pamiętać, aby zadbać tutaj o takie kwestie jak być może pokazanie użytkownikowi jakiegoś spinnera na czas ładowania komponentu. W takiej sytuacji możemy wspomóc się dość popularną biblioteką – React Loadable

React Loadable

React Loadable jest to HOC który umożliwia nam ładowanie komponentów przy użyciu właśnie co omówionego dynamicznego importu. API tej biblioteki umożliwia nam w łatwy sposób dodać komponent wyświetlany podczas ładowania zasobów. Oprócz tego możemy obsłużyć błędy, zaimplementować preloading, importować dynamicznie moduły w trybie server-side rendering oraz jeszcze wiele innych użytecznych funkcjonalności. Zainteresowanych odsyłam do dokumentacji..

Jeszcze inną biblioteką wspomagającą nas w Code Splitting w React (również obsługującą server-side rendering) jest loadable components.

React.lazy

Począwszy od wersji 16.6 React posiada już wbudowaną funkcjonalność dynamicznego importowania komponentów. Przykład użycia:

const BigTextEditor = React.lazy(() => import('./BigTextEditor'));

function MyComponent() {
  return (
    <div>
      <OtherComponent />
    </div>
  );
}

W powyższym przypadku BigTextEditor zostanie dynamicznie zaimportowany tylko wtedy gdy zostanie wywołany przez metodę render(). W przeciwnym razie import() nigdy się nie wykona. Jak widać na powyższym przykładzie metoda .lazy() przyjmuje jako argument funkcję wywołującą dynamiczny import. Dynamiczny import w tym przypadku musi zwrócić nam Promise którego wynikiem będzie moduł zawierający domyślnie eksportowany (export default) komponent Reactowy. Z racji tego, iż ładowanie komponentów zazwyczaj trwa chwilę i chcielibyśmy w tym czasie pokazać użytkownikowi jakiś wskaźnik ładowania – React.lazy praktycznie zawsze będziemy używać razem z kolejną funkcjonalnością Reacta, czyli Suspense.

Suspense

Suspense jest komponentem którego zadaniem jest wyświetlenie czegoś w miejsce dynamicznie ładujących się komponentów. Może to być np. wspomniany już wcześniej wskaźnik ładowania.

Info

W dużym skrócie Suspense pozwala nam odłożyć „na później” renderowanie części naszej aplikacji do momentu, gdy nie zostaną spełnione pewne warunki (np. do czasu gdy nie pobierzemy danych z API bądź gdy jesteśmy w trakcie dynamicznego importu komponentu).

Suspense akceptuje props o nazwie fallback, który może być dowolnym komponentem Reactowym. Ten właśnie komponent będzie widoczny na ekranie w trakcie wykonywania dynamicznego importu. Wewnątrz jednego komponentu Suspense możemy mieć kilka dynamicznie importowanych modułów:

const BigTextEditor = React.lazy(() => import('./BigTextEditor'));
const MultistepForm = React.lazy(() => import('./MultistepForm'));

function MyComponent() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <section>
          <h2>Fill the form<h2>
          <MultistepForm />
        </section>
        <section>
          <h2>Send us your feedback<h2>
          <BigTextEditor />
        </section>
      </Suspense>
    </div>
  );
}

Obsługa błędów

Nie możemy zapominać również o sytuacji w której z jakiś powodów nie uda nam się załadować dynamicznie naszych komponentów. Możemy obsłużyć taki przypadek wykorzystując kolejny komponent dostarczony wraz z Reactem – Error Boundaries. Komponent tego typu umieszczamy „nad” komponentami dynamicznymi i za jego pomocą przekazujemy użytkownikowi stosowne komunikaty o błędzie na ekranie:

import FormErrorBoundary from './FormErrorBoundary';

const BigTextEditor = React.lazy(() => import('./BigTextEditor'));
const MultistepForm = React.lazy(() => import('./MultistepForm'));

function MyComponent() {
  return (
    <div>
      <FormErrorBoundary>
        <Suspense fallback={<div>Loading...</div>}>
          <section>
            <h2>Fill the form<h2>
            <MultistepForm />
          </section>
          <section>
            <h2>Send us your feedback<h2>
            <BigTextEditor />
          </section>
        </Suspense>
      </FormErrorBoundary>
    </div>
  );
}

W którym miejscu dzielić kod?

Myślę że niemal każdy zgodzi się ze stwierdzeniem iż stosowanie code splitting w większych aplikacjach przyniesie nam wiele dobrego. Zostaje jeszcze jedna kwestia – w którym miejscu dzielić nasz kod?

Istnieją dwa najpopularniejsze podejścia:

  • route-based, czyli dzielenie na poziomie pojedynczych stron (ekranów),
  • component-based, czyli dzielenie na poziomie komponentów.

Dzielenie na poziomie stron może wydawać się dość intuicyjne, gdyż ładujemy zawartość strony tylko wtedy, gdy użytkownik na nią przejdzie. W dodatku nasi użytkownicy korzystając z innych stron w Internecie pewnie są przyzwyczajeni do tego, iż przejście między podstronami może powodować lekkie opóźnienie.

import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import React, { Suspense, lazy } from 'react';

const Home = lazy(() => import('./pages/Home'));
const PageWithTabs = lazy(() => import('./pages/PageWithTabs'));
const About = lazy(() => import('./pages/About'));

const App = () => (
  <Router>
    <Suspense fallback={<div>Loading...</div>}>
      <Switch>
        <Route exact path="/" component={Home}/>
        <Route path="/about" component={About}/>
        <Route path="/pagewithtabs" component={PageWithTabs}/>
      </Switch>
    </Suspense>
  </

Jednak co w przypadku gdy na naszej stronie mamy zakładki i w jednej z tych zakładek znajduje się ciężki edytor tekstowy? Załaduje się razem z całą stroną mimo, iż być może nie będzie w ogóle użyty.

Między innymi z tego powodu osobiście uważam iż lepszym rozwiązaniem będzie stosowanie drugiego podejścia – dzielenie na poziomie komponentów. Nawiasem mówiąc w większości routerów stworzonych dla React – ścieżka (route) jest również komponentem. Pokazanie użytkownikowi jak najszybciej nowej strony na której będzie trochę tekstu i kręcące się wskaźniki ładowania w miejsce importowanych właśnie komponentów moim zdaniem również wygląda lepiej niż lag związany z ładowanie całej nowej strony.

webpack-bundle-analyzer

Na koniec wspomnę jeszcze o bardzo fajnym narzędziu, które pozwala nam analizować pliki generowane przez webpacka – webpack-bundle-analyzer. Może to być dobry punkt startowy, aby zobaczyć które z komponentów badź zewnętrznych bibliotek zajmują najwięcej miejsca w naszym wyjściowym pliku bundle i może od wycięcia ich z głównej aplikacji zacząć cały proces optymalizacyjny. Webpack-bundle-analyzer będzie również przydatny, aby stwierdzić, czy faktycznie udało nam się zacząć dzielić jeden plik na kilka mniejszych.

Znamy już w takim razie podstawowe metody optymalizacji oraz wiemy jak działa i jak stosować code splitting. Zostały już tylko dwa ostatnie tematy które chciałbym podciągnać pod tematykę przyśpieszania Reacta a mianowicie debounce/throttling oraz windowing. Stosowny artykuł pojawi się już niedługo.

Kamil Józwik

Front end developer👨‍💻 Autor małego narzędzia dla programistów - frontbook.dev oraz autor kolejnych postów na tym blogu. Ulubiony stack to React wraz z TypeScript oraz wszystko co "nowe" w tym pięknym dynamicznym front end-owym świecie 🙂

Dodaj komentarz