Kiedy ostatnio dobrze pracowało Ci się z kodem na frontendzie? Wywiad z Mateuszem Kłosińskim
Co sprawia, że frontend często traktujemy po macoszemu? Jak takie podejście przekłada się na wydajność produktu? Na te i na wiele innych pytań odpowiedział Mateusz Kłosiński, Lead Software Engineer w gravity9.
Spis treści
Frontend to mniej szanowany element każdego cyfrowego produktu?
Pracowałem zarówno przy produktach, których kod frontendowy był bardzo dobrej jakości, jak i z takimi, przy których spędziłem tygodnie na refactoringu i optymalizacji. Więcej razy spotkałem się jednak z sytuacją, w której kod frontendowy nie był najprzyjemniejszy do pracy i odbiegał jakością od tego backendowego.
Myślę, że jest to problem, z którym zmaga się wielu programistów i wiele projektów. Warto o tym porozmawiać i zastanowić się, dlaczego tak jest. Nie zapominajmy też, że są projekty, które cechują się dobrą jakością kodu po stronie klienta. W związku z tym nie powiedziałbym, że jest to mniej szanowany element każdego produktu cyfrowego.
Skąd Twoim zdaniem bierze się przekonanie o tym, że frontend jest mniej ważny i poświęca się mu mniej czasu?
Myślę, że jednym z głównych powodów takiego stanu rzeczy jest fakt, że kod frontendowy musi zostać pobrany i uruchomiony po stronie klienta. Wyobraźmy sobie, że na frontendzie znalazła się logika do wyliczania rabatu dla klientów. Do serwera przesyłamy już gotową cenę, którą klient musi zapłacić. Kod frontendowy można łatwo zmodyfikować, więc nic nie stoi na przeszkodzie, żeby ktoś zmienił sobie cenę produktu na dowolną inną. Z tego względu nie może on zawierać żadnej ważnej logiki biznesowej, a także nie można w nim przechowywać żadnych kluczy czy haseł do połączenia z zewnętrznymi usługami. Myślę, że jest to pierwszy powód traktowania frontendu jako mniej ważnego kodu.
Do tego dochodzi jeszcze różnica w języku programowania. JavaScript jest językiem pod niektórymi względami podobnym do C# czy Javy, a pod niektórymi zupełnie odmiennym. Specyficzne mechanizmy tego języka często są obiektem żartów innych programistów. Za przykład może tu posłużyć automatyczna konwersja typów, brak statycznego typowania czy brak możliwości użycia wielu wątków.
Z tych powodów często traktuje się JavaScript jako coś prostszego i pozornie wymagającego mniej wiedzy i umiejętności.
Czym przejawia się to, że frontend czasem jest traktowany z mniejszym priorytetem?
Na backendzie najczęściej bardzo dbamy o takie rzeczy, jak czytelność kodu, implementujemy wzorce projektowe i przestrzegamy reguł takich jak SOLID. Ponadto w aplikacjach serwerowych zapewniamy również wysokie pokrycie testami. Wszystkie te praktyki poprawiają jakość kodu i powodują, że jest on mniej skomplikowany do analizy przez innych programistów. Bazując na moim doświadczeniu mogę powiedzieć, że na frontendzie albo te elementy są traktowane mniej rygorystycznie, albo ich po prostu całkowicie brakuje.
Najwięcej pracowałem z Angularem, więc pozwolę sobie na nim oprzeć przykłady. Chyba największym problemem z jakim się spotkałem był braku odpowiedniego podziału na komponenty. Elementy, które mogłyby być wspólne dla kilku widoków często były kopiowane zamiast wydzielane do dedykowanego komponentu. Skutkowało to tym, że trzeba było wyszukać miejsca, w których zduplikowany kod występuje. Następnie należy nanieść odpowiednie zmiany. Na końcu dopasować rozwiązanie do tego, że w każdym miejscu coś było zrobione trochę w inny sposób. W projekcie napisanym w ten sposób proste zmiany pochłaniają dużo czasu i łatwo jest przeoczyć miejsca do poprawy, co skutkuje brakiem spójności w interfejsie użytkownika.
Kolejnym dość ważnym problemem jest niedostosowywanie rozwiązań do tego, w jaki sposób framework renderuje wprowadzone przez nas zmiany do faktycznego kodu HTML w przeglądarce (drzewa DOM). Często, przez brak wiedzy jak działa mechanizm detekcji zmian w frameworku frontendowym, powstają niewydajne rozwiązania. Wielokrotnie, gdy pojawiał się taki problem okazywało się, że można go było dosyć łatwo rozwiązać. Na przykład w Angularze wywoływane były metody bezpośrednio z szablonów zamiast wykorzystać dedykowany Pipe do transformacji danych.
Podsumowując, brak dbałości o jakość kodu po stronie frontendu może prowadzić do długiego czasu wykonywania pozornie łatwych zadań, braków w spójności i powolnego działania interfejsu użytkownika.
Co mogłoby zmienić ten stan?
Trzeba uświadomić sobie, że frontend jest produktem, z którego bezpośrednio korzystają nasi użytkownicy. W większości nie mają oni dostępu do naszego API, nie są świadomi wyrafinowania i zaawansowania technologicznego, architektury itp., których użyliśmy po stronie serwera. W przypadku gdy interfejs użytkownika jest pełen błędów, niespójności i nie należy do najszybszych, to nawet mając najlepiej napisany backend, nasz system będzie miał niezbyt pochlebną opinię wśród użytkowników. W zależności od specyfiki projektu, to od użytkowników może zależeć czy nasz produkt będzie odnosił sukcesy, czy nie. Dlatego myślę, że warto uświadamiać zarówno biznes, jak i osoby techniczne, że frontend jest równie ważny, jak backend i trzeba dbać o jego jakość oraz wydajność.
Jakość możemy poprawić wprowadzając testy. Mamy do dyspozycji dwa rodzaje testów. Możemy testować jednostkowo pojedyncze komponenty, jak i możemy wprowadzić również testy automatyczne end-to-end, które “klikają” po naszym interfejsie użytkownika i sprawdzają czy system zachowuje się poprawnie. Dodatkowo możemy wprowadzić wymagalność, że przed wdrożeniem nowego kodu, wszystkie dotychczasowe testy muszą przechodzić.
Jeśli chodzi o wydajność to tutaj również mamy spore możliwości. W najprostszym scenariuszu możemy monitorować wydajność frontendu manualnie podczas standardowego testowania funkcjonalności. Możemy również podpiąć nasz frontend do narzędzi monitorujących, z których korzystamy w firmach, zbierać metryki i na ich podstawie weryfikować czy wszystko nadal działa w zadowalającym dla nas czasie.
Co o frontendzie powinien wiedzieć każdy backendowiec?
Myślę, że każdy backendowiec powinien wiedzieć, że pomimo wielu podobieństw, frontend często rządzi się swoimi prawami. Niektóre z nich są bardziej oczywiste, niektóre mniej. Oznacza to tyle, że nie wszystkie techniki, które z powodzeniem stosujemy po stronie backendu, mają również zastosowanie po stronie frontendu.
Dobrym przykładem jest tutaj dziedziczenie. Po stronie backendu ma ono szerokie zastosowanie i często jest uważane za dobrą praktykę. W przypadku frontendu opartego na komponentach tak nie jest. Nawet w dokumentacji Reacta jest wzmianka o dziedziczeniu komponentów i napisano tam, że w Facebooku nie znaleźli żadnego przypadku użycia, gdzie dziedziczenie by się sprawdziło. Ma to związek z tym, że komponenty z natury używają innej techniki – kompozycji. Komponenty powinniśmy tworzyć i zagnieżdżać jedne w drugich. Komunikować je między sobą powinniśmy używając wbudowanych mechanizmów np. Input/Output w Angularze.
Użycie dziedziczenia widziałem na własne oczy i sprawia ono kilka problemów. Jednym z nich jest to, że do każdego komponentu mamy przypisany szablon HTML. Dziedzicząc po innym komponencie w Angularze tracimy możliwość wykorzystania szablonu komponentu rodzica i musimy w dość pokrętny sposób odwołać się do niego. Dodatkowo dziedziczymy wszystkie pola opisane dekoratorem Input nawet jeśli w nowym komponencie nie chcemy ich używać.
Myślę, że przykład ten dobrze pokazuje, że przechodząc z backendu na frontend powinniśmy dobrze rozeznać się, które techniki znane z aplikacji serwerowych możemy z powodzeniem stosować, a których powinniśmy unikać.
Cztery lata temu przeprowadziliśmy devdebatę na temat współpracy na linii backend i frontend. Jednym z problemów wskazanych przez frontendowców był brak dokumentacji backendu. Dzisiaj to nadal istniejący problem?
Tak samo jak z jakością kodu, tak z dokumentacją wszystko zależy od projektu. Z własnego doświadczenia mogę powiedzieć jedynie, że zarówno w przypadku backendu, jak i frontendu dobra dokumentacja jest bardzo rzadkim zjawiskiem.
Główną trudnością we współpracy frontendu z backendem jest komunikacja i dokumentowanie kontraktów, którymi będą się posługiwać przesyłając dane między sobą. Tak naprawdę jest to główny (i chyba jedyny) punkt styku tych dwóch światów. Warto zadbać o jakieś miejsce, do którego oba zespoły mają dostęp i tam opisać każde API. Jakie dane przyjmuje, jakie odsyła, w jakim formacie. Dobrze jest tam również zapisać wszystkie zmiany, zwłaszcza te określane jako “breaking”. Są to zmiany takie jak usunięcie pola czy zmiana jego nazwy. Powodują one, że na pewno po drugiej stronie coś przestanie działać.
Jeśli nie mamy czasu na prowadzenie dobrej dokumentacji, ciekawym podejściem jest wystawianie API, które zwraca “mocki” zamiast rzeczywistych danych. Oznacza to, że serwer wystawia API spełniające wcześniej ustalony kontrakt, ale zwraca sztuczne dane. Dzięki temu przed właściwą implementacją funkcjonalności na backendzie, zespół frontendowy może zacząć pracę używając prawie docelowych zapytań HTTP. Z kolei zespołowi backendowemu łatwiej trzymać się ustalonego kontraktu, jeśli jest on już zaimplementowany u nich w kodzie.
Jak dbasz o wydajność frontendu?
Jak w poprzednich przypadkach, przykłady oprę na frameworku Angular. W tym przypadku mamy kilka spraw, na które warto zwrócić uwagę, jeśli chcemy, aby nasza aplikacja działała sprawnie.
Pierwszą z nich jest to, o czym już wcześniej mówiłem. Często przy prezentacji danych musimy je w jakiś sposób transformować. Przykładowo w określony sposób formatujemy datę, walutę czy po prostu musimy zebrać dane z kilku miejsc i połączyć je w jedną wartość, która oznacza coś dla użytkownika. Najprostszym sposobem, żeby to osiągnąć jest napisanie metody w klasie komponentu i użycie jej w szablonie. Niestety, ma to jedną znaczącą wadę. Z każdym cyklem detekcji zmian ta metoda będzie wywoływana, nawet jeśli jej dane wejściowe się nie zmieniły.
Angular nie jest w stanie określić, czy dla takich samych danych wejściowych wynik funkcji będzie taki sam. Musi on ją po prostu wywołać. Jeśli taka implementacja pojawi się w wielu miejscach aplikacji, wpłynie to na jej ogólną wydajność. Aby temu zaradzić, zamiast wywołania metod w HTML, powinniśmy używać rozwiązania dostarczonego przez Angular, czyli Pipe. Jest to jeden z bloków budulcowych tak jak komponent, dyrektywa czy serwis. Dostarczone są zarówno wbudowane w sam framework, jak np. te do formatowania daty czy walut, jak i możemy napisać własny. Pipe ma tą przewagę, że dla tych samych danych wejściowych zostanie wywołany tylko raz, więc jeśli one się nie zmienią to nie będzie potrzeby wykonywania kodu w nim zawartego.
Drugą rzeczą, którą możemy wprowadzić w naszym kodzie, aby poprawić jego wydajność to minimalizacja ilości subskrypcji. Kilkukrotnie ta prosta refaktoryzacja pomogła w kodzie, przy którym pracowałem. Najbardziej dotkliwe jest to, kiedy tworzymy wiele komponentów w pętli i każdy z nich subskrybuje po dane np. wywołując zapytanie HTTP lub odwołując się do globalnego store jak np. ngrx. Można zyskać dużo na wydajności zbierając potrzebne dane w komponencie rodzica i przekazując je do każdego wygenerowanego komponentu w pętli poprzez Input.
Wspomniane techniki są dosyć proste do implementacji, a mogą naprawdę pomóc w aplikacjach napisanych w Angularze.
Mateusz Kłosiński. Lead Software Engineer w gravity9. Doświadczony programista. Pracuje zarówno przy rozwiązaniach backendowych, jak i frontendowych. Przykłada dużą wagę do jakości i czytelności kodu, który tworzy. Chętnie dzieli się wiedzą z innymi i pomaga tworzyć lepsze rozwiązania. Lubi stawiać sobie wyzwania i pogłębiać wiedzę z różnych dziedzin. W wolnym czasie chodzi na długie spacery lub gra na konsoli.
Mateusz niebawem poprowadzi prelekcję podczas wydarzenia „Improve Angular components flexibility with Content Projection!” we Wrocławiu. Więcej o wydarzeniu tutaj.