Podręcznik

11. Wydajność aplikacji internetowych

11.2. Porady optymalizacyjne

Projektowanie serwisu WWW to nie tylko dbanie o jego funkcjonalność i bezpieczeństwo ale także wydajność. Zdefiniowana wspomnianymi wcześniej metrykami zależna jest od architektury aplikacji ale przede wszystkim od sposobu jej dostarczania do odbiorcy. Spójrzmy więc na podstawowe możliwości optymalizacji, które łatwo jest wdrożyć w praktyce.

Użycie protokołu HTTP/2 i HTTP/3

Podstawowym protokołem wykorzystywanym do dostarczania stron internetowych jest protokół http (Hypertext Transfer Protokol) w wersji 1.1 [52]. Więcej o historii można przeczytać w [51]. Podstawowym założeniem jakie przyświecało autorom było maksymalne uproszczenie implementacji. Efektem jest bardzo uniwersalny protokół, który jednocześnie niezbyt efektywnie korzysta z zasobów. Nie posiada kompresji nagłówków (możliwa jest kompresja treści) co zwiększa rozmiar transmisji, szczególnie w kontekście wspomnianych wcześniej nagłówków CSP. Umożliwia jedynie sekwencyjne wykonywanie żądań z oczekiwaniem na rezultat, co wymaga wykonywania wielu połączeń do serwera docelowego dla uzyskania zrównoleglenia i uzyskania krótszego czasu ładowania strony. Aby zaadresować te problemy proponowano różne rozwiązania jak protokół SPDY [53], na bazie rozwiązań którego powstała nowa wersja protokołu http – HTTP/2 [54]. Wprowadza on warstwę pośrednią pomiędzy warstwą sieciową a klasycznym protokołem HTTP/1.1 transparentnie zachowując semantykę tego protokołu wprowadzając, format pośredni umożliwiający multipleksację wielu strumieni (zapytanie-odpowiedź) w postaci binarnie kodowanych ramek w ramach jednego połączenia. Pozwala to przede wszystkim na wykonywanie wielu żądań bez potrzeby oczekiwania na odpowiedź. Na rysunku poniżej przedstawiono koncepcję rozwiązania. W rzeczywistości multipleksacja odbywa się w mniejszych, przeplatanych ramkach.

Multipleksacja żądań i odpowiedzi w HTTP/2

Głównym efektem jest lepsze wykorzystanie połączenia sieciowego i zmniejszenie opóźnień. Dodatkowo umożliwia nadanie priorytetów zasobom przez twórców strony, co powoduje, że w przypadku wspomnianej multipleksacji w pierwszej kolejności przesyłane są zasoby o wyższym priorytecie.

Najnowsza wersja protokołu HTTP – wersja 3 bazuje na protokole QUIC [55] i jako warstwy transportowej używa domyślnie protokołu UDP (co pozwala na uniknięcie niektórych problemów wydajnościowych) choć wspiera też transmisję TCP.

Użycie systemów dystrybucji treści i kontrola pamięci pośredniej

Jednym z problemów szybkości dostarczania treści jest czas potrzebny na dostarczenie pakietu z miejsca A do miejsca B. Przyjrzyjmy się typowemu przypadkowi nawiązania połączenia TLS1.3 pomiędzy klientem w Polsce i serwerem w Japonii z czasem transportu pakietu na poziomie 140ms w jedną stronę. Jak widać na poniższym rysunku czas ten sięga >800ms do otrzymania pierwszych danych.

TLS 1.3 - przepływ pakietów

Najprostszym rozwiązaniem tego problemu jest skorzystanie z systemu pośredniego – sieci dystrybucji treści CDN (Content Distribution Networks). Jest wielu dostawców takich usług o zasięgu globalnym. Stanowią oni warstwę pamięci pośredniej pomiędzy serwerem właściwym a odbiorcą danych, a dzięki rozmieszczeniu serwerów dostawcy w różnych regionach skraca to czas dostarczania pakietów danych do odbiorców. Przykładowo Coludflare posiada 310 lokalizacji (grudzień 2023) na całym świecie [57] i deklaruje opóźnienia do 50ms dla 95% populacji.

Aby mechanizm pośrednika działał poprawnie, niezbędne jest jednak wskazanie które zasoby, na jaki czas mają być przechowywane i serwowane z pamięci podręcznej. Kontrolę zapewnia wspomniany już wcześnień nagłówek Cache-Control, który kontroluje sposób działania pamięci podręcznych (w tym pamięci podręcznej przeglądarki). Przykłady można znaleźć w dokumentacji Cloudflare [57]. Nieprawidłowa konfiguracja może uniemożliwić odświeżanie treści w odpowiednim czasie a także powodować przypadkowe przechowywanie treści poufnej na serwerach pośredniczących.

Optymalizacja zapytań DNS i ilości pobieranych zasobów

Jednym z problemów na jakie można napotkać to opóźnienia w pobieraniu treści strony wynikające z faktu, że dane pobieramy z wielu miejsc. Każde pierwsze zapytanie do nowej domeny poprzedzane jest zapytaniem DNS w celu rozwiązania odpowiedniej nazwy. Jak pokazuje raport DNSPerf [56] typowy czas odpowiedzi serwerów DNS waha się od 20 do 120ms. Oznacza to, że pobranie dowolnego, kolejnego zasobu z nowego miejsca wymaga dodatkowego czasu. Sumaryczny narzut zależy także od ilości równoległych zapytań. Dzięki mechanizmom pamięci podręcznej lokalnego resolvera DNS dotyczy to tylko pierwszego pobrania zasobu z nowego miejsca.

W tym aspekcie należy też zwrócić uwagę na to, czy zasoby nie są serwowane za pośrednictwem przekierowań (kody http 301, 302) z osobnych domen. Samo wykorzystanie przekierowania wnosi narzut czasowy poprzez dodatkowe zapytanie HTTP a przekierowanie do dodatkowej domeny dodaje do tego czas potrzebny na rozwiązanie nazwy. Stąd proste zalecenie:

  • unikać używania przekierowań, o ile nie są niezbędne,
  • minimalizować liczbę domen których rozwiązanie jest niezbędne do pobrania treści,

Po stronie serwera problem zapytań DNS może, paradoksalnie być znacznie bardziej szkodliwy. Należy pamiętać, by w aplikacji nie próbować niepotrzebnie rozwiązywać adresów IP podłączających się klientów na adresy symboliczne z wykorzystaniem mechanizmu reverse-DNS. Może tak się zdarzyć np. w przypadku nieprawidłowej konfiguracji dzienników serwera WWW. W tym przypadku, mechanizm pamięci podręcznej DNS nie jest zbyt przydatny, bo każdy nowy klient to nowy adres do rozwiązania.

Minimizacja, kompresja, kolejność dostarczania zasobów, leniwe ładowanie

Jedną z najprostszych rzeczy, jaką można zrobić aby poprawić wydajność dostarczania aplikacji, to odpowiednio przygotować treść, która zostanie przesłana:

  • minimizacja (minification) – to proces przekształcania kodu źródłowego, JS, CSS, HTML polegający na usunięciu z niego wszystkich zbędnych elementów, w szczególności niepotrzebnych białych znaków, a także skracanie nazw zmiennych w kodzie dla uzyskania mniejszej objętości danych,
  • bundling – łączenie wielu plików danego typu (np. skryptów JS) w pojedyncze pliki dla zmniejszenia ilości żądań niezbędnych do pobrania strony,
  • optymalizacja czcionek – należy zadbać o minimalną ilość czcionek na stronie,
  • optymalizacja obrazów – jednym z najczęstszych błędów jest dostarczanie obrazów w zbyt dużej rozdzielczości i skalowanie ich atrybutami po stronie przeglądarki, proces optymalizacji obejmuje także dobranie odpowiednich formatów i poziomów kompresji tych obrazów, warto przyjrzeć się m.in. formatowi WebP [60], który zapewnia możliwości formatów JPEG i PNG przy wyższej kompresji i wspierany jest w większości współczesnych przeglądarek [59],
  • odpowiednia kolejność umieszczenia bloków treści w kodzie html tak, aby wyeliminować elementy blokujące możliwość renderowania treści, pamiętajmy, że bloki typu
    <link rel="stylesheet" href="styles.css">
    <script src="scripts.js"></script>

wstrzymują proces rysowania do momentu ich załadowania,

  • opóźnione doładowywanie obrazów, które nie są niezbędne lub nie są widoczne w danych chwili – tutaj trzeba jednak uważać na efekt przesuwania się treści w trakcie doładowywania, który znacząco pogarsza efekt wizualny i użyteczność strony,
  • włączenie kompresji treści o charakterze tekstowym.

Więcej szczegółowych zaleceń można znaleźć w [61].