Decyzja o PWA zwykle pojawia się wtedy, gdy produkt ma działać szybko na mobile, ale budżet i czas nie pozwalają na dwie natywne aplikacje. W praktyce najwięcej zyskują serwisy z częstymi powrotami użytkownika, jak e-commerce, portale treściowe, panele klienta i narzędzia B2B. Żeby ocenić sens wdrożenia, trzeba rozdzielić trzy warstwy: instalację, działanie offline oraz kontrolę nad siecią i cache.
Co to jest Progressive Web App (PWA)?
PWA to aplikacja webowa, która zachowuje się jak aplikacja instalowana, ale jest dostarczana przez przeglądarkę. Termin Progressive Web App został spopularyzowany przez Google w 2015 roku jako podejście stopniowe, gdzie funkcje uruchamiają się zależnie od możliwości urządzenia. Rdzeń techniczny stanowią trzy elementy: strona działająca po HTTPS, plik manifestu oraz Service Worker. Jeśli któryś element jest niedostępny, aplikacja nadal działa jako zwykła strona, tylko bez części funkcji systemowych.
Co daje PWA?
Na urządzeniu użytkownika PWA może pojawić się jako ikona na ekranie startowym i uruchamiać się w osobnym oknie, co zmniejsza tarcie powrotu do usługi. Dzięki kontrolowaniu cache można skrócić czas do pierwszej interakcji na słabszych sieciach, o ile zasoby są dobrze podzielone i wersjonowane. Powiadomienia push bywają dostępne, ale zależą od przeglądarki i polityk platformy, więc nie należy ich traktować jako gwarantowanej funkcji. Rekomendacja: wybierz PWA, gdy najważniejsze ścieżki użytkownika da się zamknąć w kilku ekranach i nie wymagają stałego dostępu do sprzętu, jak Bluetooth LE czy zaawansowane czujniki. PWA nie zadziała jako pełny zamiennik natywnych aplikacji tam, gdzie potrzebujesz stabilnego działania w tle, rozbudowanych integracji systemowych albo dystrybucji wyłącznie przez sklep z aplikacjami.
Kiedy PWA ma sens (a kiedy nie)?
Zanim wejdziesz w manifest i service worker, zrób szybki test produktu: czy PWA poprawi powroty użytkownika i odporność na sieć w 2-3 kluczowych scenariuszach. Poniżej masz prosty podział, który zwykle oszczędza tygodnie pracy i rozczarowań.
PWA ma sens, gdy
- użytkownik wraca często (konto, koszyk, panel, lista obserwowanych) i instalacja realnie skraca drogę do usługi,
- pierwsze ekrany da się przyspieszyć przez cache (shell + zasoby wersjonowane),
- offline ma być "awaryjny" (fallback + ostatnie dane), a nie pełną kopią produktu,
- aplikacja jest głównie "webowa" (formularze, listy, treści), bez ciężkich integracji systemowych,
- dystrybucja przez URL jest wystarczająca (a sklep ma być opcją, nie wymaganiem).
PWA zwykle nie ma sensu, gdy
- wymagane jest stabilne działanie w tle (ciągłe procesy, intensywne sync, usługi działające bez aktywnego UI),
- kluczowe funkcje opierają się o natywne SDK i API specyficzne dla platformy (w praktyce "bez tego nie działa"),
- produkt musi być dystrybuowany wyłącznie przez sklep, a web jest tylko dodatkiem,
- offline ma obejmować większość funkcji i duży zakres danych z konfliktem synchronizacji (to już osobny projekt),
- nie masz kontroli nad cache i wersjonowaniem (np. miks wielu domen, brak hashy, brak spójnej polityki HTTP).
Manifest aplikacji
Manifest to plik JSON opisujący, jak aplikacja ma wyglądać po instalacji i jak ma się uruchamiać. W praktyce liczą się dwa poziomy: minimum instalowalności oraz detale, które poprawiają jakość instalacji.
Minimum instalowalności
Ustaw name, short_name, start_url, display oraz ikony PNG co najmniej 192x192 i 512x512. Jeśli start_url prowadzi do ekranu wymagającego natychmiastowego logowania, łatwo o wrażenie "pustego startu" po instalacji, więc lepiej kierować do stabilnego widoku (np. dashboard z obsługą stanu niezalogowanego).
Detale, które robią różnicę
Dodaj ikonę z purpose: maskable, żeby systemy mobilne nie psuły kadrowania logo. Coraz częściej warto też ustawić pole id, żeby instalacja była stabilniejsza i łatwiej rozróżniać aplikację przy zmianach start_url lub struktury. Weryfikację poprawności manifestu najprościej zrobić w Chrome DevTools w zakładce Application, a warunki instalowalności najszybciej wychodzą w audycie Lighthouse.
Service Worker od podstaw
W tej części skupiamy się na tym, jak Service Worker realnie zachowuje się w przeglądarce i co z tego wynika dla kodu aplikacji. Najwięcej problemów pojawia się na styku cyklu życia, cache i aktualizacji, więc rozbijamy temat na konkretne decyzje implementacyjne. Przykłady odnoszą się do typowych aplikacji webowych, gdzie liczy się przewidywalność działania offline i kontrola nad wersjami zasobów.
Rejestracja i zakres działania
Rejestrację wykonuje się w kodzie strony przez navigator.serviceWorker.register('/sw.js'), a przeglądarka pobiera skrypt tylko w kontekście HTTPS lub localhost. Zakres (scope) wynika z lokalizacji pliku i opcjonalnego parametru scope, więc sw.js w katalogu /app/ nie przechwyci żądań z /, jeśli nie ustawisz tego świadomie. Jeśli aplikacja działa pod subścieżką lub za reverse proxy, sprawdź w DevTools zakładkę Application, czy scope pokrywa faktyczne URL-e, bo inaczej część zasobów ominie Service Workera.
Cykl życia: install, activate, fetch
Podczas install zwykle zapisujesz do Cache Storage zasoby startowe, ale instalacja zakończy się niepowodzeniem, jeśli obietnice w event.waitUntil odrzucą się choć raz. W activate sprzątasz stare cache i przejmujesz kontrolę nad klientami, a brak czyszczenia powoduje narastanie danych i ryzyko serwowania nieaktualnych plików. W fetch decydujesz, czy odpowiedź ma pochodzić z sieci czy z cache, i to tu najłatwiej wprowadzić błąd polegający na cache’owaniu odpowiedzi, które nie powinny być utrwalane (np. personalizowane HTML).
Cache Storage i wersjonowanie
Cache Storage to magazyn odpowiedzi HTTP zarządzany z poziomu Service Workera, więc wersjonowanie najczęściej robi się przez nazwy typu app-static-v3 i app-runtime-v3. Rekomendacja: trzymaj osobne cache dla zasobów budowanych (CSS/JS) i dla runtime (np. obrazki), bo mają inne zasady unieważniania i inne ryzyko "starej treści". Jeśli używasz bundlera, dopilnuj, aby pliki statyczne miały w nazwie hash treści, bo wtedy cache busting działa mechanicznie i nie wymaga zgadywania, co się zmieniło.
Strategie obsługi żądań
Najczęściej spotkasz network-first dla HTML (żeby szybciej łapać aktualizacje) oraz cache-first dla wersjonowanych plików JS/CSS (żeby przyspieszyć start i odciążyć sieć). Stale-while-revalidate polega na natychmiastowym zwróceniu cache i równoległym odświeżeniu w tle, co jest dobre dla zasobów, gdzie drobne opóźnienie aktualizacji jest akceptowalne, np. listy obrazów. Strategia nie zadziała, jeśli serwer wymaga zawsze świeżych danych per użytkownik, bo cache może utrwalić odpowiedzi zależne od sesji, a wtedy trzeba wyłączyć cache dla tych endpointów lub oprzeć się o kontrolę po stronie serwera (nagłówki i segmentację odpowiedzi).
Aktualizacje i problem "starego cache"
Nowy Service Worker nie zacznie obsługiwać ruchu od razu, bo domyślnie czeka, aż wszystkie karty ze starą wersją zostaną zamknięte, co użytkownik odczuwa jako "aplikacja nie aktualizuje się". skipWaiting() i clients.claim() przyspieszają przejęcie kontroli, ale to decyzja ryzykowna, bo możesz podmienić logikę w trakcie działania aplikacji i doprowadzić do niespójności między HTML a JS. Rekomendacja: wdroż komunikat o dostępnej aktualizacji i przeładuj kontrolowanie (np. po kliknięciu), a dodatkowo czyść cache po activate według jawnej listy dozwolonych nazw, żeby stare wersje nie zostawały na urządzeniu.
Nagłówki HTTP a cache w Service Workerze
Service Worker może zignorować semantykę nagłówków cache, jeśli sam zapiszesz odpowiedź do Cache Storage, więc musisz świadomie zdecydować, czy respektujesz Cache-Control, ETag i Vary. Jeśli serwer ustawia Cache-Control: no-store dla danych wrażliwych, nie zapisuj ich do cache nawet "na chwilę", bo to łamie intencję polityki i utrudnia audyt. Gdy opierasz odświeżanie o ETag, pamiętaj, że warunkowe żądania (If-None-Match) mają sens tylko przy network-first lub revalidate, a przy czystym cache-first nie zobaczysz 304 i nie odświeżysz zasobu.
Strategia cache w pigułce: domyślne ustawienia (i dlaczego)
Najłatwiej podejść do cache jak do mapy zasobów: HTML, pliki wersjonowane (JS/CSS), API i media (obrazy/fonty) mają inne ryzyka i inne cele. Jedna strategia „dla wszystkiego” prawie zawsze kończy się albo starą wersją aplikacji, albo wolnym startem.
HTML (dokumenty, routing, widoki) → network-first + offline fallback
Cel: łapać aktualizacje, a przy braku sieci nie pokazywać błędu przeglądarki. HTML jest najbardziej "niebezpieczny" do cache’owania, bo bywa personalizowany i mocno zależy od wersji JS.
JS/CSS z hashem w nazwie → cache-first
Cel: maksymalnie szybki start i powtarzalność. Jeśli pliki mają hash treści, cache busting dzieje się sam: nowa wersja ma nowy URL, więc nie musisz zgadywać, co unieważnić.
API → zależnie od danych (najczęściej network-first lub stale-while-revalidate)
network-first: gdy liczy się świeżość (np. statusy, ceny, dane konta).
stale-while-revalidate: gdy ważniejsza jest szybkość, a opóźnienie aktualizacji jest akceptowalne (np. listy, katalogi, treści).
Offline: strategie i dane
Warstwa offline to nie "tryb samolotowy jako bonus", tylko decyzja o tym, co użytkownik ma zobaczyć i co może zrobić, gdy sieć jest słaba albo znika. Najpierw zdefiniuj minimalny zestaw ekranów i akcji, które mają działać bez internetu, bo próba "pełnej aplikacji offline" zwykle kończy się rosnącą złożonością i niespodziankami w cache. W praktyce offline dobrze działa tam, gdzie użytkownik konsumuje treść (np. ostatnio oglądane), przegląda listy lub wypełnia formularz, który da się wysłać później. Praktyczna zasada na start: cache’uj shell aplikacji i przewidywalne HTML-e, a API buforuj tylko tam, gdzie nie ma wariantów per użytkownik lub masz twardą kontrolę nad segmentacją i unieważnianiem.
Fallback dla HTML i sensowny ekran offline
Jeśli HTML jest network-first, musisz mieć plan, co zwracasz, gdy sieć nie odpowiada. Najprostsze rozwiązanie to pre-cache strony offline (fallback) i zwracanie jej, gdy fetch dla dokumentu się nie uda, zamiast pokazywać błąd przeglądarki. Rekomendacja: offline page powinna mieć jasny komunikat, przycisk ponowienia oraz linki do tych widoków, które są dostępne z cache (np. ostatnio otwarte sekcje), bo inaczej użytkownik utknie.
Dane lokalne: co trzymasz i gdzie
Trzymaj lokalnie tylko to, co ma wartość w offline i co nie zwiększa ryzyka bezpieczeństwa. localStorage jest prosty, ale nie nadaje się do większych struktur i nie jest dobrym miejscem na dane wrażliwe. Dla danych aplikacyjnych lepiej sprawdza się IndexedDB, bo pozwala przechowywać kolekcje, indeksować i pracować na większym wolumenie bez blokowania UI. Rekomendacja: rozdziel cache zasobów (Cache Storage) od danych (IndexedDB), bo mieszanie tych światów utrudnia debug i wersjonowanie.
Kolejka operacji i wysyłka po powrocie sieci
Jeśli użytkownik ma móc coś zrobić offline (np. dodać komentarz, wysłać formularz, zapisać zmianę), potrzebujesz kolejki operacji: zapisujesz intent, walidujesz dane lokalnie i oznaczasz rekord jako "do wysłania". Po odzyskaniu połączenia wysyłasz operacje w kontrolowany sposób (retry z limitem i backoff), a po sukcesie zdejmujesz je z kolejki. Typowa pułapka to wielokrotne wysyłanie tego samego żądania po reconnect, więc dodaj idempotency key albo identyfikator operacji, żeby backend umiał rozpoznać duplikat.
Konflikty i wersjonowanie danych
Offline oznacza, że dane mogą się zmienić po obu stronach: użytkownik coś edytuje lokalnie, a serwer w międzyczasie dostaje inną wersję. Najprostsza ochrona to wersjonowanie rekordów (np. updatedAt lub numer wersji) i odrzucanie zapisu, jeśli wersja na serwerze jest nowsza. Jeśli konflikty są realne, zaplanuj regułę rozstrzygania: albo "ostatni zapis wygrywa", albo ręczna decyzja użytkownika, albo merge dla wybranych pól. Bez tej decyzji offline będzie działał "na demo", ale rozjedzie się w produkcji.
Bezpieczeństwo offline: co może zostać na urządzeniu
Offline łatwo kusi, żeby cache’ować wszystko, ale to zwiększa skutki XSS i ryzyko zostawienia danych na współdzielonym urządzeniu. Nie zapisuj do cache odpowiedzi zawierających dane wrażliwe, a jeśli część danych musi być dostępna offline, ogranicz ich zakres i czas życia oraz czyść je po wylogowaniu. Traktuj offline jako rozszerzenie UX, nie jako magazyn danych użytkownika.
Wydajność i budżet zasobów
Najpierw ustal budżet zasobów, czyli limity na wagę i liczbę plików dla krytycznej ścieżki, bo bez tego optymalizacja zamienia się w przypadkowe poprawki. W praktyce często startuje się od celów typu: pierwszy widok do pobrania poniżej 200-300 KB skompresowanego JS na mobile, ale traktuj to jako punkt wyjścia i weryfikuj w danych rzeczywistych użytkowników, a nie jako uniwersalną normę. Dziel kod na porcje ładowane na żądanie i unikaj jednego dużego bundla, bo opóźnia to parsowanie i blokuje interakcję. Ustaw cache dla zasobów statycznych z długim max-age i wersjonowaniem w nazwie pliku, a dla API stosuj strategię zależną od danych, na przykład network-first dla świeżości lub stale-while-revalidate dla szybkości.
Budżet bez zgadywania: podziel na trzy koszyki
JS: limituj koszt startu (parsowanie/wykonanie), tnij zależności i ładuj moduły dopiero wtedy, gdy są potrzebne.
Obrazy: niech pierwszy widok ma małe, dopasowane pliki (format + wymiary), bo to zwykle największy transfer na mobile.
Fonty: ogranicz liczbę wariantów i wag, bo fonty potrafią blokować render i psuć odczucie szybkości.
Mierz w polu, nie tylko lokalnie, raportując LCP, INP oraz CLS per widok, bo średnia dla całej aplikacji ukrywa problemy. Jeśli aplikacja jest ciężka przez biblioteki UI, rozważ redukcję zależności lub renderowanie po stronie serwera, bo sama etykieta PWA nie naprawi kosztów CPU na słabszych telefonach.
Bezpieczeństwo i HTTPS
HTTPS jest wymagane, bo bez niego przeglądarki blokują Service Workera i część API, a dodatkowo chroni przed podmianą treści w sieci. Certyfikat TLS można uzyskać bezpłatnie, ale trzeba pilnować automatycznego odnawiania i poprawnej konfiguracji łańcucha, bo błędy certyfikatu odcinają aplikację od aktualizacji. Ustaw HSTS, aby wymusić HTTPS po pierwszej wizycie, lecz wdrażaj ostrożnie, bo błędna konfiguracja może zablokować dostęp do subdomen. Zabezpiecz nagłówki, w szczególności Content-Security-Policy, aby ograniczyć XSS, bo w PWA z cache atak może utrwalić złośliwy skrypt na dłużej. Nie przechowuj w cache odpowiedzi zawierających dane wrażliwe, a jeśli musisz, stosuj krótkie TTL i mechanizmy unieważniania po wylogowaniu. Dla autoryzacji preferuj tokeny o krótkim czasie życia i rotacji, bo długowieczne tokeny w pamięci przeglądarki zwiększają skutki przejęcia sesji.
Publikacja i dystrybucja
Dystrybucja przez URL i moment instalacji
Najprostszy kanał to zwykły adres URL, ale instalacja zadziała przewidywalnie dopiero, gdy przeglądarka rozpozna spełnione warunki i pokaże prompt w odpowiednim kontekście. Dopilnuj manifestu z name, short_name, start_url, display oraz zestawem ikon PNG co najmniej 192x192 i 512x512, bo brak tych pól często kończy się brakiem propozycji instalacji lub brzydką ikoną na ekranie. Rekomendacja: testuj instalację na Chrome/Edge i Safari osobno, bo heurystyki "kiedy zaproponować instalację" różnią się, a na iOS użytkownik zwykle instaluje przez "Dodaj do ekranu początkowego" bez automatycznego promptu.
Pakowanie do sklepów: TWA i alternatywy
Jeśli potrzebujesz obecności w Google Play, typową ścieżką jest Trusted Web Activity, czyli kontener Androida, który uruchamia Twoją aplikację webową w pełnoekranowym Chrome bez paska adresu. TWA wymaga powiązania domeny z aplikacją (Digital Asset Links) i zgodności z politykami sklepu, ale to nadal web uruchamiany w kontrolowanym środowisku, a nie przepustka do natywnych API. W praktyce TWA nie zadziała dobrze, gdy aplikacja musi działać na urządzeniach bez aktualnego Chrome albo gdy krytyczne funkcje zależą od integracji stricte natywnej, więc przed publikacją zrób testy na kilku wersjach Androida i na urządzeniach z różnymi producentami.
Aktualizacje i cache jako element produktu
Aktualizacje wdrażasz jak w webie, ale Service Worker może trzymać starą wersję plików, więc użytkownik zobaczy zmiany dopiero po odświeżeniu lub po zamknięciu wszystkich kart. Wprowadź wersjonowanie zasobów (np. nazwy plików z hashem) oraz kontrolę cyklu życia Service Workera, a do tego komunikat "Dostępna nowa wersja" z przyciskiem odświeżenia, bo inaczej wsparcie dostanie zgłoszenia o "niedziałających poprawkach". Rekomendacja: traktuj strategię cache jako decyzję produktową i ustal, które ekrany mogą tolerować opóźnioną aktualizację, a które muszą wymuszać przeładowanie, bo przy zmianach API lub schematu danych stara wersja UI potrafi generować błędy trudne do odtworzenia.
Checklista wdrożenia
Poniższa checklista ma pomóc przejść przez wdrożenie. Skupia się na elementach, które najczęściej psują instalowanie, działanie offline i stabilność po aktualizacjach. Traktuj ją jak zestaw testów akceptacyjnych: każdy punkt ma jasne kryterium zaliczenia.
HTTPS na wszystkich domenach
Zweryfikuj, że każda domena i subdomena (www, bez www, CDN, domeny obrazów i API) serwuje HTTPS bez mixed content oraz z poprawnym łańcuchem certyfikatów, bo pojedynczy zasób po HTTP potrafi zablokować część funkcji w przeglądarce.
Przekierowania i HSTS
Ustaw trwałe przekierowanie 301 z HTTP na HTTPS i rozważ HSTS (HTTP Strict Transport Security) tylko jeśli masz pewność, że wszystkie hosty będą stale dostępne po HTTPS, bo błędna konfiguracja potrafi odciąć użytkowników do czasu wygaśnięcia polityki.
Manifest: ikony i maskowalność
Sprawdź w manifeście zestaw ikon co najmniej 192×192 i 512×512 oraz wariant maskable, bo bez tego systemy mobilne potrafią przyciąć logo lub użyć rozmytej ikony przy instalacji.
Manifest: start_url i scope
Ustal start_url i scope tak, aby po uruchomieniu z ekranu głównego aplikacja lądowała w właściwym miejscu i nie "wypadała" do przeglądarki przy nawigacji poza scope.
Service Worker: strategia per zasób
Zastosuj różne strategie cache dla HTML, API, obrazów i fontów (np. network-first dla HTML, stale-while-revalidate dla statyków), bo jedna strategia dla wszystkiego zwykle kończy się albo starym UI, albo wolnym startem.
Cache: wersjonowanie i unieważnianie
Wprowadź wersjonowanie cache i procedurę unieważniania (np. zmiana nazwy cache przy release oraz czyszczenie starych wpisów w activate), bo inaczej użytkownicy zostaną na "pół-aktualizacji" z niespójnymi plikami.
Aktualizacje: komunikat i kontrola
Zaplanuj, jak informujesz o nowej wersji i kiedy ją aktywujesz (np. przy następnym uruchomieniu lub po potwierdzeniu), bo wymuszenie natychmiastowego przeładowania może przerwać formularz lub koszyk.
Web Vitals w polu
Mierz Web Vitals w danych rzeczywistych użytkowników (RUM) i wysyłaj je do analityki z rozróżnieniem na typ urządzenia i sieć, bo wyniki z labu nie pokażą problemów na słabszych telefonach i 3G.
Offline: ekrany krytyczne
Zdefiniuj zachowanie offline dla najważniejszych ekranów (np. ostatnio oglądane, koszyk, dane profilu) oraz jasne komunikaty o ograniczeniach, bo "biały ekran" jest zwykle skutkiem braku fallbacku dla HTML lub API.
PWA jest najbardziej opłacalne wtedy, gdy możesz poprawić powroty użytkownika i odporność na sieć bez wchodzenia w pełny świat natywnych SDK. Najszybciej widać efekty, gdy projekt zaczyna od budżetu zasobów, strategii cache i jasnych zasad aktualizacji, a dopiero potem dokłada instalowalność. Jeśli Twoje wymagania obejmują głębokie integracje systemowe lub ciężkie przetwarzanie w tle, potraktuj PWA jako uzupełnienie, a nie zamiennik. Dobrze wdrożone PWA to przede wszystkim dyscyplina w wydajności, bezpieczeństwie i wersjonowaniu, a nie pojedyncza funkcja do "włączenia".

Komentarze