W zeszłym miesiącu pozbyliśmy się Service Worker z naszej aplikacji. Nie była to decyzja oczywista i dojrzewała w naszym zespole przez prawie pół roku. Dziś chcę się podzielić z Wami naszą historią i trochę przekonać Was, że nie wszyscy potrzebujemy Service Workera.

Jeśli jesteście kompletnymi nowicjuszami w zakresie PWA i pierwszy raz słyszycie o Service Workerze, to zanim przejdziecie dalej, zapoznajcie się chociaż z podstawową dokumentacją na temat Service Workera przygotowaną przez Google. Bez tego dalsza lektura może się dla Was okazać ciężka i pełna niedopowiedzeń.

* Service Worker pozostał w naszej aplikacji, ale teraz zajmuje się tylko Push Notyfikacjami i jest obsługiwany prawie całkowicie przez bibliotekę dostarczona przez firebase.

1. Nasza aplikacja nie wspiera prawdziwego Offline

Zdecydowanie nie jest to najważniejszy powód, dla którego usunęliśmy Service Workera, ale w mojej opinii mocno rzutuje na kontekst pozostałych punktów. Na papierze Service Worker może być wykorzystany tylko do cachowania plików, a wspomaganie trybu offline można uznać jako poboczną funkcjonalność. Z taką narracją spotkałem się w sieci i trochę w myśl tej niej działał nasz Service Worker. W Vived nigdy nie znaleźliśmy odpowiedniej motywacji i budżetu potrzebnych do zaimplementowania w pełni funkcjonalnej wersji offline. Ba, nawet nie było jej widać na horyzoncie naszej road mapy. Nasze wsparcie dla wersji offline, sprowadzało się więc do opakowania strony błędu w nasz własny UI. Było to rozwiązanie ładne, ale czy warte swojej ceny?

Tryb offline w Vived

Jeśli w Waszej aplikacji wymaganiem biznesowym jest tryb offline, to przykro mi, ale nie będziecie w stanie pozbyć się całkowicie Service Workera. Możecie drastycznie go uprościć i przenieść właściwą obsługę offline do kodu aplikacji, ale zawsze zostaniecie, co najmniej z małym kawałkiem Service Workera, przechowującym index.html i niezbędne bundle JavaScriptowe.

2. Service Worker jest zbyt elastyczny

Z pozoru ciężko uznać dużą elastyczność za wadę (w końcu frontendowa społeczność uwielbia elastycznego reacta i stroni od mocno ustanardowanego Angulara). Trzeba jednak pamiętać, że duża swoboda idzie w parze z mnogością możliwych rozwiązań tego samego problemu. Często pochodną tej swobody jest bardziej skomplikowane API. Tak jest właśnie w przypadku Service Workera. W dokumentacji, twórcy otwarcie przyznają, że jednym z głównych celów przy projektowaniu API, był brak założeń co do tego, w jak  będzie ono używane. Niestety da się to odczuć na każdym kroku. API jest pełne eventów i ekstremalnie elastyczne, ale implementacja nawet najbardziej standardowych scenariuszy wymaga dogłębnego zrozumienia, jak działa Service Worker i stworzenia kawałka, zawiłego dla postronnych, kodu. Oznacza to, że ciężko rozproszyć wiedzę o Service Workerach w zespole i niezwykle ciężko uniknąć bugów, które powstaną, gdy ktoś zacznie refactorować nasz kod.

https://www.w3.org/TR/service-workers/#motivations

Prawdopodobnie u wielu z Was zaświeciła się teraz czerwona lampka - dlaczego nie skorzystaliśmy z frameworka wspomagającego tworzenie Service Workerów? Na naszej drodze korzystaliśmy zarówno z WorkBoxa jak i Angular Service Worker. Jak wspomniałem wcześniej, do Push Notyfikacji używamy Service Workera dostarczonego przez Firebase. Te frameworki/biblioteki rozwiązują sporą część opisanych powyżej problemów, ale wprowadzają swoją warstwę konfiguracji. Poza tym nigdy nie udało nam się oprzeć całkowicie na zewnętrznych bibliotekach i zawsze musieliśmy odrobinę ‘przylukrować’ nasz Service Worker. Ta odrobina lukru zazwyczaj wystarczyła, żeby odpowiednio skomplikować całość i wprowadzić, co najmniej kilka bugów.

3. Użytkownik nie da Ci drugiej szansy

Jeśli Service Workera używacie, aby zasilić standardową mobilną aplikację, to prawdopodobnie nie macie się, o co martwić. Stworzyliście nową niesamowitą funkcjonalność i użytkownik nie zauważył jej w dniu releasu? Nic nie szkodzi. Użytkownik najprawdopodobniej wróci do aplikacja za kilka dni i wtedy będzie miał okazję podziwiać nasz kunszt programistyczny. Sytuacja zmienia się diametralnie, w sytuacji gdy oprócz aplikacyjnej części chcemy wspierać bardziej statyczne funkcjonalności. Za przykład niech posłużą oferty, które jeszcze niedawno obsługiwaliśmy w Vived. Były one integralną częścią aplikacji, ale umożliwialiśmy również dzielenie się nimi z zarejestrowanymi użytkownikami i aktywnie promowaliśmy je w social mediach. W takim przypadku użytkownik nie był związany z aplikacją i mógł trafiać do nas w sporych odstępach czasu. Oznacza to, że nasza praca nie docierała na czas do użytkowników i obserwowali oni przestarzałą wersję naszej strony. Intensywne iteracje nad coraz lepszą prezentacją ofert, nie trafiały do użytkowników końcowych. Zdecydowanie nie jest to scenariusz, jaki chcecie przerabiać w początkowej fazie pracy nad produktem, gdy posiadanie dobrej pętli zwrotnej jest kluczowe dla efektywnych iteracji nad funkcjonalnością.

4. Mam już dość tłumaczeniu reszcie zespołu: zamknij wszystkie karty i odśwież...

Z pozoru wydaje się to śmiesznym problemem, ale długofalowo powodowało niesamowicie dużo frustracji. Żeby bezpiecznie podmienić Service Worker obsługujący aplikację, należy odczekać aż ten się zainstaluje, następnie zamknąć wszystkie otwarte karty z aplikacją i odświeżyć jedyną niezamkniętą.

Nasz Product Owner ma 32GB ramu i prawie całkowicie wypełnia je Chrome. Nawet jeśli w trakcie spotkanie uda mu się zlokalizować wszystkie karty z Vived, to nie będzie w stanie ich zamknąć. Część z nich celowo jest otwarta, żeby szybko wrócić do pracy sprzed spotkania. Oczywiście możecie napisać instrukcję, jak poradzić sobie z problemem przy pomocy DevToolsów, ale czy naprawdę chcemy uczyć naszego Product Ownera jak obsługiwać DevToolsy? W DevToolsach chrome jest jeszcze opcja ‘Bypass for network’, ale włączając ją narażamy się na pominięcie całego szeregu błędów. Osobiście zdarzało mi się zapomnieć o wyłączeniu tego checkboxa po developmencie nowej funkcjonalności, co prowadziło do wypuszczenia na produkcję aplikacji z błędami w Service Workerze.

Drugim problem, jaki mieliśmy w zespole z Service Workerem, było zdefiniowanie kiedy odświeżyć stronę. Czas pobierania Service Workera zależy od aktualnej jakości internetu. Ciężko zliczyć, ile czasu zmarnowaliśmy dyskutując o nieistniejących błędach, które ostatecznie okazywały się zbyt szybką próbą odświeżenia Service Workera i testowaniem starej wersji aplikacji.

5. Przeciętny użytkownik webowej aplikacji nie zdaje sobie sprawy, że zapisujesz coś na jego dysku

Ze względów opisanych powyżej bardzo zależało nam na poinformowania użytkowników, kiedy nowa wersja aplikacji będzie gotowa do uruchomienia (tj. Service Worker jest gotowy przejąć obsługę strony). Jeśli chodzi o standardowe PWA zainstalowane na dysku, to wyświetlenie małego banera zachęcającego do odświeżenia jest wzorcem, do którego użytkownicy są przyzwyczajeni. Natywne aplikacje często aktualizują się po uruchomieniu lub wyświetlają baner zachęcający do przejścia do sklepu, tak więc mówimy tutaj o bardzo podobnym doświadczeniu. Sytuacja zmienia się drastycznie, kiedy mówimy o aplikacji uruchomionej w przeglądarce. Przecięty użytkownik nie zdaje sobie sprawy z istnienia PWA czy Service Workera. Wyświetlenie takiego banera jest nieintuicyjne, bo podświadomie oczekujemym, że strony które oglądamy będą aktualne.

Obsługa aktualizacji Service Workera w Vived

6. Aplikacja raz opublikowana z Service Workerem będzie tam już na zawsze

Sytuacja w tym przypadku wygląda właściwie identycznie, jak w przypadku aplikacji mobilnych. Kiedy build trafi do sklepu, to nie mamy sposobu, żeby wycofać go z telefonów, na których został już zainstalowany. W naszej aplikacji przygotowaliśmy scenariusz zmuszający użytkowników do aktualizacji w przypadku wystąpienia krytycznego błędu. Opieramy go na angualarowych interceptorach i dodaniu odpowiedniego nagłówka do odpowiedzi naszego Backendu.

Obsługa blokowania zbyt starych wersji aplikacji w Vived

Z pozoru nie widać tutaj szczególnego problemu. Należy zadać sobie jednak pytanie, czy jesteśmy w stanie zrobić cokolwiek, żeby umożliwić użytkownikowi uniknięcie tego niechcianego procesu? Jeśli chodzi o aplikacje mobilne, to niestety nie znaleźliśmy lepszego rozwiązania (na szczęście z naszych obserwacji wynika, że większość użytkowników korzysta z automatycznych aktualizacji). Jeśli chodzi o aplikacje Webowe, to uważamy, że udało nam się znaleźć takie rozwiązanie i sprowadza się ono do serwowania użytkownikowi zawsze najnowszej wersji aplikacji.

7. Dobrze skonfigurowany cache rozwiązuje te same problemy

Oczywiście nie pozbyliśmy się z naszej aplikacji całkowicie funkcjonalności cache’a. Wolny internet często przedstawiany jest jako problem mniej rozwiniętych technologicznie krajów i łatwo go zignorować, jeśli wasza aplikacja ma trafiać do jednej z bogatszych grup zawodowych w środkowoeuropejskim kraju z dobrą infrastrukturą internetową. Na szczęście moje sporadyczne podróże Pendolino i internet niektórych członków naszego zespołu, skutecznie przypominają nam, że wolny internet jest problemem, który może dotknąć każdego i nie możemy zamieść go pod dywan (z tego miejsca serdecznie pozdrawiam Artura, któremu od pół roku za tydzień będą montować światłowód). Na szczęście istnieją inne sposoby na mitygację tego problemu niż Service Worker.

Naszą odpowiedzią na usunięcie Service Workera było poświęcenie większej uwagi przeglądarkowym cache. Współczesne funkcjonalności przeglądarkowego cache’a zawdzięczamy Facebookowi, który już 2007 roku chciał rozwiązać problem pobierania wielokrotnie tych samych plików. Co ciekawe Facebook założył wtedy osobne feature requests do Firefoxa i Chrome. Obie przeglądarki zaimplementowały swoje wersje rozwiązania. Dziś powszechnym standardem stał się nagłówek zaproponowany przez Google (`max-age=0, must-revalidate`  ), ale w specyfikacji do dziś widnieje też przygotowany przez Mozille `Cache-Control: immutable`,  który nigdy nie został zaimplementowany w Chrome.

Nie chcę, żeby ten materiał przekształcił się w typowy tutorial, ale postaram się przemycić Wam kilka wskazówek z naszej konfiguracji. Po pierwsze index.html nie jest cachowany. Oznacza to, że przeglądarka zawsze wykonuje jedno zapytanie po niewielki plik, w którym znajdują się odnośniki do JavaScriptowych bundli. Jeśli w cache nie ma odpowiednich plików, to są one pobierane (użytkownik otwiera aplikację po raz pierwszy lub właśnie wypuściliśmy nową wersję aplikacji). Natomiast jeśli są już w cache, to serwujemy je bezpośrednio stamtąd. Nie JavaScriptowe pliki odpytują serwer o zmiany i zaciągają body tylko jeśli takowe nastąpiły.

W powyższej implementacji jest pewna pułapka. W przypadku Service Workera pobieraliśmy wszystkie bundle wyplute przez kompilator. Teraz zaciągamy tylko pliki potrzebne do wyrenderowania obecnej strony. Na szczęście Angular pozwala obsłużyć tą sytuację przez doładowanie reszty plików w optymalnym momencie poprzez wykorzystanie odpowiedniej strategii preloadingu.


Czy powinniście teraz udać się prosto do Waszego kodu i usunąć wszystkie Service Workery jakie staną Wam na drodze? Najprawdopodobniej nie... Service Worker w wielu miejsach radzi sobie całkiem nieźle.  Mam natomiast nadzieję, że zarówno nasza historia i błędy jakie popełnialiśmy po drodze, pozwolą Wam podejmować decyzje w Waszych projektach.

PS: Jeśli macie odmienne przemyślenia na temat Service Workera to zapraszamy do dyskusja na facebooku 🔥