Optymalizacja interakcji do kolejnego wyrenderowania

Dowiedz się, jak zoptymalizować liczbę interakcji z witryną przy okazji kolejnego wyrenderowania.

Interakcja z kolejnym wyrenderowaniem (INP) to stabilne podstawowe dane internetowe, które pozwalają ocenić ogólną responsywność strony na interakcje z użytkownikiem przez obserwowanie opóźnienia wszystkich kwalifikujących się interakcji, które mają miejsce w całym okresie trwania wizyty użytkownika na danej stronie. Ostateczna wartość INP to najdłuższa zaobserwowana interakcja (czasem ignorując wartości odstające).

Aby zadbać o wygodę użytkowników, czas interakcji z kolejnym wyrenderowaniem w witrynie powinien wynosić 200 milisekund. Aby mieć pewność, że w przypadku większości użytkowników osiągasz ten cel, dobrym progiem jest 75 centyl wczytań stron w podziale na urządzenia mobilne i komputery.

Dobre wartości INP to 200 milisekund lub mniej, niskie – więcej niż 500 milisekund, a wszystkie pozostałe wymagają poprawy.

W zależności od witryny interakcje mogą być bardzo ograniczone lub wcale – np. strony zawierające głównie tekst i obrazy bez elementów interaktywnych lub z niewieloma elementami. Z kolei w przypadku witryn takich jak edytory tekstu czy gry mogą występować setki, a nawet tysiące interakcji. W obu przypadkach, gdy wartość INP jest wysoka, może to negatywnie wpłynąć na wrażenia użytkownika.

Ulepszenie INP wymaga czasu i wysiłku, ale w efekcie nagrody są lepsze dla użytkowników. W tym przewodniku omówimy sposób na poprawę wartości INP.

Dowiedz się, co powoduje niskie wartości INP

Zanim naprawisz powolne interakcje, musisz otrzymać dane, dzięki którym dowiesz się, czy wartość INP witryny jest niska lub wymaga poprawy. Gdy zdobędziesz te informacje, możesz przejść do modułu, aby zacząć diagnozować powolne interakcje i pracować nad rozwiązaniem problemu.

Znajdź powolne interakcje w terenie

Optymalizowanie INP najlepiej zacząć od danych zgromadzonych. Dane pól od dostawcy usługi Real User Monitoring (RUM) dostarczają nie tylko wartości INP strony, ale też danych kontekstowych, które pokazują, która konkretna interakcja odpowiadała za samą wartość INP, czy interakcja miała miejsce podczas wczytywania strony czy po jej wczytaniu, jaki jest rodzaj interakcji (kliknięcie, naciśnięcie lub dotknięcie) i inne cenne informacje.

Jeśli nie korzystasz z usług dostawcy RUM do pobierania danych pól, zapoznaj się z przewodnikiem po danych pól INP, aby uzupełnić luki za pomocą raportu na temat użytkowania Chrome (CrUX). CrUX to oficjalny zbiór danych programu podstawowych wskaźników internetowych, który zawiera ogólne podsumowanie danych dotyczących milionów witryn, w tym wartości INP. Raport CrUX często nie podaje jednak danych kontekstowych, które można uzyskać od dostawcy RUM, aby ułatwić analizę problemów. Z tego powodu nadal zalecamy, aby w miarę możliwości witryny korzystały z usług dostawcy RUM lub wdrożyli własne rozwiązanie RUM, aby uzupełnić to, co jest dostępne w systemie CrUX.

Diagnozowanie powolnych interakcji w module

Najlepiej zacznij testy w module, gdy uzyskasz dane terenowe, które wskazują na powolne interakcje. Jeśli nie ma danych terenowych, istnieją pewne strategie wykrywania powolnych interakcji w module. Strategie te obejmują śledzenie typowych przepływów pracy użytkowników i testowanie ich interakcji podczas jej ładowania, a także interakcję z nią podczas wczytywania strony – gdy wątek główny jest często najbardziej intensywny – w celu wyeliminowania powolnych interakcji w trakcie tego istotnego etapu korzystania ze strony.

Optymalizuj interakcje

Gdy już wykryjesz powolną interakcję i możesz ją ręcznie odtworzyć w module, następnym krokiem jest jej zoptymalizowanie. Interakcje można podzielić na 3 fazy:

  1. Opóźnienie danych wejściowych, które rozpoczyna się, gdy użytkownik zainicjuje interakcję ze stroną, i kończy się, gdy uruchamiane są wywołania zwrotne tej interakcji.
  2. Czas przetwarzania, czyli czas potrzebny na ukończenie wywołań zwrotnych zdarzeń.
  3. Opóźnienie prezentacji, czyli czas potrzebny przeglądarce na wyświetlenie następnej klatki, która zawiera wizualny wynik interakcji.

Suma tych 3 faz to całkowity czas oczekiwania na interakcję. Każda faza interakcji zwiększa jej całkowity czas oczekiwania, dlatego warto wiedzieć, jak możesz ją zoptymalizować, aby trwała jak najkrócej.

Identyfikowanie i zmniejszanie opóźnienia wejścia

Gdy użytkownik wchodzi w interakcję ze stroną, pierwszą częścią tej interakcji jest opóźnienie wejściowe. Zależnie od innych działań na stronie opóźnienia wprowadzania danych mogą być znaczne. Może to być spowodowane aktywnością występującą w wątku głównym (np. z powodu ładowania skryptów, analizowania i kompilowania), obsługi pobierania, funkcji licznika czasu, a nawet innych interakcji, które występują w krótkich odstępach czasu i nachodzą na siebie.

Niezależnie od źródła opóźnienia wejścia interakcji warto zredukować opóźnienie do minimum, aby interakcje mogły jak najszybciej rozpocząć wykonywanie wywołań zwrotnych zdarzeń.

Związek między oceną skryptu a długimi zadaniami podczas uruchamiania

Kluczowym aspektem interaktywności w cyklu życia strony jest jej uruchamianie. Po wczytaniu strona będzie początkowo renderowana. Pamiętaj jednak, że wyrenderowanie strony nie oznacza, że zakończyła się wczytywanie. W zależności od liczby zasobów potrzebnych do pełnego działania strony może się zdarzyć, że użytkownicy będą próbowali wchodzić z nią w interakcję, gdy się ona nadal wczytuje.

Jedną z rzeczy, które mogą wydłużyć opóźnienie danych wejściowych interakcji podczas wczytywania strony, jest ocena skryptu. Gdy plik JavaScript zostanie pobrany z sieci, przeglądarka musi jeszcze coś zrobić, zanim zostanie uruchomiona. To zadanie obejmuje analizę skryptu w celu sprawdzenia, czy jego składnia jest prawidłowa, skompilowanie go do kodu bajtowego i wykonanie go.

W zależności od rozmiaru skryptu może to spowodować wprowadzanie długich zadań w wątku głównym, co opóźni reagowanie przeglądarki na inne interakcje użytkownika. Aby Twoja strona reagowała na działania użytkowników podczas jej wczytywania, musisz wiedzieć, co możesz zrobić, aby zmniejszyć prawdopodobieństwo wykonania długich zadań w trakcie wczytywania strony, tak aby działała szybko.

Optymalizacja wywołań zwrotnych zdarzeń

Opóźnienie wejściowe to tylko pierwsza część pomiarów INP. Trzeba też zadbać o to, aby wywołania zwrotne zdarzenia przeprowadzane w odpowiedzi na interakcję użytkownika mogły jak najszybciej być ukończone.

Często zgłaszaj się do wątku głównego

Najlepszą ogólną radą z optymalizacją wywołań zwrotnych zdarzeń jest wykonywanie przy nich jak najmniejszego nakładu pracy. Logika interakcji może być jednak skomplikowana, a Ty możesz ograniczyć ilość wykonywanej przez nich pracy.

Jeśli uznasz, że tak jest w przypadku Twojej witryny, możesz spróbować podzielić zadania wykonywane w wywołaniach zwrotnych zdarzeń na osobne zadania. Dzięki temu praca zbiorcza nie może przekształcić się w długie zadanie, które zablokuje wątek główny, przez co umożliwia to innym interakcjom, które w innym przypadku musiałyby czekać na szybsze uruchomienie wątku głównego.

setTimeout to jeden ze sposobów podziału zadań, ponieważ przekazane do niego wywołanie zwrotne działa w nowym zadaniu. Możesz użyć funkcji setTimeout samodzielnie lub podzielić jej użycie na osobną funkcję, aby uzyskać większą ergonomię.

Beztroskie uzyskanie jest lepsze niż wcale, ale można to zrobić w bardziej szczegółowy sposób w wątku głównym, który obejmuje generowanie wyników dopiero natychmiast po wywołaniu zwrotnym zdarzenia, które aktualizuje interfejs, dzięki czemu logika renderowania może działać szybciej.

Wydaj, aby umożliwić renderowanie szybciej

Bardziej zaawansowana technika uzyskiwania zysków obejmuje strukturę kodu w wywołaniach zwrotnych zdarzeń w celu ograniczenia działań do poziomu logiki wymaganej do zastosowania aktualizacji wizualnych w następnej klatce. Resztę można odłożyć do późniejszego zadania. Wywołania zwrotne są nie tylko proste i szybkie, lecz także skróci czas renderowania interakcji, ponieważ nie zezwala na blokowanie aktualizacji wizualnych na kodzie wywołania zwrotnego zdarzenia.

Wyobraź sobie na przykład edytor tekstu sformatowanego, który formatuje tekst w trakcie pisania, ale w zależności od tego, co wpisujesz w interfejsie, aktualizuje inne elementy interfejsu (takie jak liczba słów, wyróżnianie literówek czy inne ważne informacje wizualne). Ponadto w aplikacji może być konieczne zapisanie napisanej treści, aby w razie zamknięcia aplikacji i powrotu nie została utracona żadna praca.

W tym przykładzie w odpowiedzi na znaki wpisywane przez użytkownika muszą wystąpić 4 czynności. Jednak przed wyświetleniem kolejnej klatki trzeba wykonać tylko pierwszy element.

  1. Wpisz w polu tekstowym tekst wpisany przez użytkownika i zastosuj wymagane formatowanie.
  2. Zaktualizuj część interfejsu, w której wyświetla się bieżąca liczba słów.
  3. Uruchom logikę, aby sprawdzić pisownię.
  4. Zapisz najnowsze zmiany (lokalnie lub w zdalnej bazie danych).

Odpowiedni kod może wyglądać mniej więcej tak:

textBox.addEventListener('input', (inputEvent) => {
  // Update the UI immediately, so the changes the user made
  // are visible as soon as the next frame is presented.
  updateTextBox(inputEvent);

  // Use `setTimeout` to defer all other work until at least the next
  // frame by queuing a task in a `requestAnimationFrame()` callback.
  requestAnimationFrame(() => {
    setTimeout(() => {
      const text = textBox.textContent;
      updateWordCount(text);
      checkSpelling(text);
      saveChanges(text);
    }, 0);
  });
});

Poniższa wizualizacja pokazuje, jak odłożenie niekrytycznych aktualizacji do następnej klatki może skrócić czas przetwarzania, a tym samym ogólny czas oczekiwania na interakcję.

Prezentacja interakcji z klawiaturą i kolejnych zadań w 2 sytuacjach. Na ilustracji powyżej zadanie o znaczeniu krytycznym dla renderowania i wszystkie kolejne zadania w tle są wykonywane synchronicznie do momentu pojawienia się możliwości wyświetlenia klatki. Na przykładzie dolnej części operacji wykonuje się najpierw zadanie o krytycznym znaczeniu dla renderowania, a następnie w wątku głównym, aby wcześniej zaprezentować nową klatkę. Później zostaną uruchomione zadania w tle.
Kliknij powyższy obraz, aby wyświetlić wersję w wysokiej rozdzielczości.

Chociaż użycie funkcji setTimeout() w wywołaniu requestAnimationFrame() w poprzednim przykładzie kodu jest wprawdzie pewną ezoteryką, jest to jednak skuteczna metoda, która działa we wszystkich przeglądarkach i gwarantuje, że niekrytyczny kod nie będzie blokować następnej ramki.

Staraj się nie ścierać układu

Przesunięcie układu, czasem nazywane wymuszonym układem synchronicznym, to problem z wydajnością renderowania, w którym układ występuje synchronicznie. Dzieje się tak, gdy zaktualizujesz style w JavaScript, a potem odczytasz je w tym samym zadaniu – jest w nim wiele właściwości, które mogą powodować szarpanie układu.

Wizualizacja układanki pokazana w panelu wydajności Narzędzi deweloperskich w Chrome.
Przykład uderzania układu graficznego w panelu wydajności Narzędzi deweloperskich w Chrome. Zadania renderowania, które obejmują odrzucanie układu, są oznaczone czerwonym trójkątem w prawym górnym rogu części stosu wywołań, często oznaczonym etykietą Ponownie oblicz styl lub Układ.

Trafianie w układy zmniejsza wydajność, ponieważ po zaktualizowaniu stylów i natychmiastowym wysłaniu wartości tych stylów w JavaScript przeglądarka jest zmuszona do pracy z układem synchronicznym. W przeciwnym razie musiałaby czekać na asynchroniczną później po zakończeniu wywołań zwrotnych zdarzeń.

Minimalizuj opóźnienie prezentacji

Opóźnienie prezentacji interakcji obejmuje czas od zakończenia wywołań zwrotnych zdarzenia interakcji do momentu, w którym przeglądarka jest w stanie wyrenderować następną ramkę przedstawiającą wynikowe zmiany wizualne.

Minimalizuj rozmiar DOM

Jeśli DOM strony jest niewielki, renderowanie zwykle szybko się kończy. Jednak gdy modele DOM stają się bardzo duże, renderowanie zazwyczaj skaluje się wraz ze wzrostem rozmiaru DOM. Związek między renderowaniem a rozmiarem DOM nie jest liniowy, ale renderowanie dużych DOM wymaga więcej pracy niż małych DOM. Duży DOM stanowi problem w 2 przypadkach:

  1. Podczas wstępnego renderowania strony, gdy duży DOM wymaga dużej ilości pracy, by wyrenderować początkowy stan strony.
  2. W odpowiedzi na interakcję użytkownika, gdzie duży DOM może spowodować bardzo kosztowne aktualizacje renderowania, a tym samym wydłużyć czas potrzebny przeglądarce na wyświetlenie następnej klatki.

Pamiętaj, że w niektórych przypadkach nie można znacznie ograniczyć dużych DOM. Istnieją sposoby, aby zmniejszyć rozmiar DOM, na przykład spłaszczenie DOM lub dodanie do niego DOM podczas interakcji z użytkownikiem, aby utrzymać mały rozmiar początkowego DOM. Te techniki mogą jednak być bardzo ambitne.

Użyj content-visibility, aby leniwe renderowanie elementów poza ekranem

Jednym ze sposobów na ograniczenie ilości pracy związanej z renderowaniem podczas wczytywania strony i renderowaniem w odpowiedzi na interakcje użytkownika jest korzystanie z właściwości CSS content-visibility, która w efekcie sprowadza się do leniwego renderowania elementów zbliżających się do widocznego obszaru. Efektywne korzystanie z elementu content-visibility wymaga pewnej praktyki, ale warto sprawdzić, czy w efekcie krótszy czas renderowania może poprawić wartość INP strony.

Pamiętaj o kosztach wydajności w przypadku renderowania kodu HTML przy użyciu JavaScriptu

W przypadku kodu HTML następuje analiza HTML i po zakończeniu analizy kodu HTML jako DOM. Przeglądarka musi zastosować do niego style, wykonać obliczenia układu, a następnie wyrenderować ten układ. To nieunikniony koszt, ale sposób renderowania kodu HTML ma znaczenie.

Gdy serwer wysyła kod HTML, trafia on do przeglądarki jako strumień. Strumieniowe przesyłanie danych oznacza, że odpowiedź HTML z serwera jest wysyłana partiami. Przeglądarka optymalizuje sposób obsługi strumienia, stopniowo analizując przychodzące fragmenty i renderując je krok po kroku. Jest to optymalizacja wydajności, która polega na tym, że przeglądarka co jakiś czas i automatycznie pobiera pliki podczas wczytywania strony, a Ty możesz to robić bezpłatnie.

Pierwsze wejście w dowolną witrynę zawsze wymaga napisania pewnej ilości kodu HTML, jednak popularne podejście zaczyna się od minimalnej ilości na początku fragmentu kodu HTML, a następnie do wypełniania obszaru treści wykorzystuje się JavaScript. Kolejne aktualizacje tego obszaru treści są też wynikiem interakcji użytkowników. Jest to tzw. model aplikacji jednostronicowej (SPA). Jedną z wad tego wzorca jest to, że renderowanie kodu HTML za pomocą JavaScriptu na kliencie nie tylko generuje koszt przetwarzania JavaScriptu w celu utworzenia tego kodu, ale także nie wyświetla pliku przez przeglądarkę do czasu zakończenia analizy i renderowania kodu HTML.

Pamiętaj jednak, że nawet witryny, które nie są aplikacjami SPA, prawdopodobnie będą częściowo renderowane za pomocą JavaScriptu w wyniku interakcji. Zasadniczo jest to w porządku, o ile nie renderujesz dużych ilości kodu HTML na kliencie, co może opóźnić wyświetlenie następnej klatki. Ważne jest jednak zrozumienie wpływu takiego podejścia przy renderowaniu kodu HTML w przeglądarce na wydajność oraz jego wpływu na responsywność witryny na dane wprowadzane przez użytkowników, jeśli renderujesz dużą ilość kodu HTML przy użyciu JavaScriptu.

Podsumowanie

Ulepszanie wartości INP witryny jest procesem iteracyjnym. Jeśli naprawisz powolne interakcje w polu, jest duża szansa, że – zwłaszcza jeśli strona zapewnia dużo interaktywności – zaobserwujesz inne powolne interakcje i trzeba też je zoptymalizować.

Kluczem do poprawy wartości INP jest ich utrzymanie. Z czasem możesz zadbać o responsywność strony w miejscu, w którym użytkownicy będą zadowoleni z udostępnianych przez Ciebie treści. Możliwe też, że podczas opracowywania nowych funkcji dla użytkowników konieczne może być przejście przez ten sam proces optymalizacji interakcji pod ich kątem. Wymaga to czasu i wysiłku, ale zajmie Ci to dużo czasu i wysiłku.

Baner powitalny z kanału Unsplash autorstwa Davida Pisnoya, zmodyfikowany zgodnie z licencją Unsplash.