Małe aplikacje – sposób na łatwiejszą pracę i szybsze dostarczanie nowych funkcjonalności
Na początku był monolit
Ciekaw jestem, ilu z Was pamięta swoje pierwsze, szkoleniowe projekty developerskie. Ich celem było nauczenie podstaw różnorodnych zagadnień. Na przykład podłączenia się do źródła danych, bezpiecznej obsługi ich na backendzie, czy tworzenia schludnego widoku dla klienta. Nawet jeśli poszliście o krok dalej i już staraliście się stosować separację warstw w Waszej aplikacji, najpewniej i tak wszystko to znajdowało się w jednym projekcie, w jednym repozytorium.
W tamtym momencie nawet nie zdawaliście sobie sprawy, że jesteście świadkami narodzin kolejnego monolitu – struktury ściśle połączonych ze sobą elementów całej aplikacji. Oczywiście nie stanowiło to żadnego problemu, ponieważ projekt ten służył Wam do nauczenia się czegoś nowego. Na wiedzę związaną z architekturą czas miał przyjść nieco później. Niemniej wiele lat temu w podobny sposób stawiano fundamenty praktycznie każdej aplikacji. Z czasem nad ich monolitycznym kodem zaczęło pracować coraz więcej developerów, a projekty rozrastały się do niebotycznych rozmiarów.
Pierwsze mikroserwisy
Dlatego w pierwszych latach obecnego wieku zaczęło formować się pojęcie mikroserwisów. Jest to koncept architektoniczny, zgodnie z którym aplikacja składa się z wielu usług, o bardzo wąskiej odpowiedzialności. Zazwyczaj zamykają się one w jednej domenie biznesowej.
W ten sposób większe aplikacje zaczęły przyjmować formę zbioru modułów, a działy developerskie – zespołów domenowych lub produktowych. Pozytywnie wpłynęło to na łatwość tworzenia oprogramowania i ogólną wydajność pracy zespołów. Ich praca odbywała się w niemal całkowitej separacji. Developerzy mogli skupiać się na swoim wycinku biznesu, nie martwiąc się o funkcjonowanie całości.
Monolity zbudowane z twardej skały
Niestety, pomimo niewątpliwych zalet takiej architektury większość dużych organizacji do dziś obciążona jest jakąś formą monolitu. Zazwyczaj są to najstarsze i najważniejsze części systemu. Po ich utworzeniu zazwyczaj nie ma potrzeby żadnej modyfikacji. Wymagają one jedynie prac utrzymaniowych związanych np. z aktualizacją wersji wykorzystywanego oprogramowania.
Oczywiście, większość produktów, nad którymi pracujemy w HL Tech to zazwyczaj nowe funkcjonalności, ale niektóre zespoły mają okazję zetknąć się także z kodem napisanym kilkanaście lat temu. W takim przypadku naszym zadaniem jest odłupanie części monolitu i bezpieczne przeniesienie go (czyt. zazwyczaj przepisanie) do osobnego modułu. Przypomina to trochę operację na żywym organizmie. Dlatego przed przystąpieniem do prac trzeba poświęcić sporo czasu na dokładne poznanie funkcjonującego już produktu. I tutaj bonus. Poprzez kontakt z kilkunastoletnim kodem mamy doskonały podgląd na efekt historycznych decyzji – tych dobrych, ale z perspektywy czasu też tych nietrafionych. Jest to dla nas bardzo rozwojowe doświadczenie, ponieważ obserwacji takich nie da się szybko poczynić na projektach pisanych od zera.
Granulacja na frontendzie
Podejście z dzieleniem na mniejsze stosujemy także w mojej specjalizacji – na frontendzie. No bo pomyślcie — użytkowników systemu zazwyczaj można podzielić na kilka grup, a każda z nich będzie korzystała z innej jego części. Czy nie brzmi to jak dobry sposób podziału platformy na moduły? W ten sposób wchodzimy w temat architektury mikro-frontendów. Około 4 miesiące temu wraz z Łukaszem Fiszerem miałem okazję dokładniej ją zaprezentować podczas Dev.js Summit 2022.
Architektura mikro-frontendów bez dwóch zdań wspomaga wydajność codziennej pracy developerów. Dla klientów i ich potrzeb jest z kolei niewidoczna. Nie wpływa bowiem na łatwość czy szybkość działania aplikacji. Na szczęście śledząc nowinki ze świata developmentu widać, że pojawiające się technologie coraz lepiej wspierają także te potrzeby. Przyjrzyjmy się części z nich.
Pobieraj tylko to, co użytkownik aktualnie potrzebuje
Nowoczesne aplikacje frontendowe w większości wykorzystują podejście zwane Client Side Rendering (CSR). Oznacza to, że wchodząc na wskazany adres url, przeglądarka najpierw pobiera cały kod Java Script, a następnie generuje widok strony.
Niestety produkcyjne wersje większości aplikacji pisanych w React to tak naprawdę dwa duże pliki Java Script. Wymusza to na kliencie pobranie całego kodu, włącznie ze stronami, których być może nawet nie ma zamiaru przeglądać. Powoduje to nadmierny ruch w Internecie, a wyliczenia na większym zakresie danych opóźniają moment wyświetlenia widoku.
Jednym z łatwiejszych i popularniejszych usprawnień tego procesu jest podejście zwane lazy loading. Polega ono na tym, że w momencie wejścia na stronę, przeglądarka pobiera tylko to, co jest potrzebne do jej wyświetlenia – kod frameworka i aktualnego widoku. W ten sposób można przyspieszyć start aplikacji nawet o kilkaset milisekund.
Rendering odmieniony przez wszystkie przypadki
Gdy zorientowano się, że mniejsza liczba wyliczeń Java Script przyspiesza start aplikacji, zaczęto zadawać sobie pytanie: czy aby na pewno wyświetlenie każdego elementu musi być efektem działania skryptu? Okazało się, że nie. Proces ten można usprawnić, w czym pomogło podejście Island architecture.
Zgodnie z nim widok każdej strony można podzielić na odizolowane od siebie obszary. Za przykład niech posłuży stopka aplikacji. Jest ona niezależna od całej reszty, a dodatkowo przez miesiące nie podlega żadnym zmianom, wygląda i działa dokładnie tak samo na każdej z podstron. To otwiera drogę do techniki Server Side Rendering (SSR) w jej najbardziej wydajnym kształcie. Zgodnie z nią serwer tylko raz generuje statyczny kod HTML komponentu i każdemu klientowi wysyła zapisany w wewnętrznej pamięci efekt tej operacji. W ten sposób przeglądarka klienta nie musi wykonywać kosztownych wyliczeń Java Script, otrzymuje finalny kod, który może bezpośrednio wyświetlić na ekranie.
Zagadnienie to można rozszerzyć nawet na całe strony. Zwłaszcza te, które nie wymagają bieżącej komunikacji z backendem. Świetnie sprawdza się to w obszarach podobnych do blogów – artykułach, elementach typu Frequently Asked Questions czy prezentacyjnych kartach produktu. W takim przypadku generujemy kod HTML całej strony i tym samym niemal do zera ograniczamy czasochłonne wyliczenia Java Script po stronie klienta. To podejście nazywamy Static Site Generation (SSG).
Inne sposoby na lepszą wydajność
Lazy loading przyspiesza czas wyświetlenia widoku dzięki rezygnacji z pobierania danych aktualnie nam niepotrzebnych. Gdy jednak klient chce wejść na inną stronę, jej kod musi zostać dociągnięty z serwera. Na szczęście nowe trendy na rynku pozwalają na przyspieszenie tego procesu w taki sposób, że staje się on prawie niezauważalny.
Ciekawe i wielopoziomowe rozwiązanie dostarcza nowy framework o nazwie Remix. Przykładowo — jego twórcy zauważyli, że od czasu najechania kursorem myszy nad przycisk, do jego naciśnięcia przez użytkownika, zwyczajowo mija kilkaset milisekund. Jeśli przycisk ten odpowiada za przejście do nowej strony, kod z nią związany możemy zacząć pobierać już na akcji hover, zamiast – jak dotychczas – na akcji click.
Framework udostępnia też możliwości łatwego sterowania zaawansowanym keszowaniem. Dzięki temu treści statyczne (np. grafika, CSS, HTML) odkładane są w punktach dostępowych Content Delivery Network (CDN). W przypadku dostawców o rozbudowanych sieciach, punkty wejściowe do niej mogą znajdować się nawet w kilku miejscach kraju. Oznacza to, że jeśli jakiś klient wszedł na daną stronę, każdy kolejny z jego okolicy dostęp do tej samej treści uzyska już w przeciągu kilkudziesięciu milisekund.
Jeśli jednak musimy wyświetlić dane „na żywo”, wymagające ciągłej komunikacji z serwerem, Remix architektonicznie faworyzuje wspomnianą wcześniej Island architecture. W ten sposób każda część strony wywołuje zapytanie po dane niezbędne do wyświetlenia w jej obszarze. Żądanie inicjuje się natychmiastowo, wykonuje w sposób równoległy, a pobrane dane pojawiają się w widoku niezależnie od siebie.
Wszystkie te zabiegi mają jeden wspólny cel — zmniejszenie czasu od wejścia na stronę do wyświetlenia zgromadzonych na niej danych. Tam, gdzie można, klient otrzymuje wygenerowaną wcześniej statyczną treść. Natomiast funkcjonalności wymagające wyliczeń na serwerze są istotnie przyspieszone, a czas spędzony na ich przeprocesowanie blokuje tylko niewielkie części aplikacji.
Dziel i bądź wydajny
Wszystko to pokazuje jak dzielenie na mniejsze zakresy i odpowiedzialności, pozytywnie wpływa na całościową wydajność. Co ciekawe, zasada ta zdaje się sprawdzać wszędzie.
Od strony organizacyjnej — pracując w jednej domenie, nie muszę znać i rozumieć wszystkich zasad biznesowych obowiązujących w firmie. Zaczynając nowy projekt, proces jego produkcji staram się podzielić na etapy, a te następnie na mniejsze zadania. Dzięki temu łatwiej jestem w stanie zrozumieć każdy ich aspekt, wiarygodnie wycenić czas realizacji a prace zamknąć w regularne sprinty.
Od strony specjalistycznej – jako frontend developer nie muszę znać wszystkich szczegółów z zakresu backendu, devops, bezpieczeństwa czy zarządzania bazami danych. Mogę skupić się na wysokiej specjalizacji w swoim technologicznym zakresie.
Od strony developerskiej — tworząc małe aplikacje, jestem w stanie bardzo szybko ogarnąć ich kod. Niewielka odpowiedzialność i tym samym poziom skomplikowania funkcjonalności pozytywnie wpływają na ich wydajność i bezpieczeństwo. W ten sposób mogę szybciej dostarczyć nowe rozwiązania i treści do naszych klientów.