Optymalizacja interakcji do kolejnego wyrenderowania

Dowiedz się, jak zoptymalizować czas od interakcji do kolejnego wyrenderowania w witrynie.

Interakcja do kolejnego wyrenderowania (INP) to stabilny podstawowy wskaźnik internetowy, który ocenia ogólną responsywność strony na interakcje użytkowników na podstawie obserwacji czasu oczekiwania na wszystkie kwalifikujące się interakcje, które występują w całym okresie wizyty użytkownika na stronie. Końcowa wartość INP to najdłuższa zaobserwowana interakcja (czasami z pomijaniem wartości odstających).

W trosce o wygodę użytkowników witryny powinny mieć czas między interakcją do kolejnego wyrenderowania o długości maksymalnie 200 milisekund. Aby mieć pewność, że w przypadku większości użytkowników ten cel zostanie osiągnięty, dobrym prógem pomiaru jest 75 centyl wczytywania stron z podziałem na urządzenia mobilne i komputery.

Dobre wartości INP to 200 milisekund lub mniej, złe wartości to ponad 500 milisekund, a wszystkie wartości pośrednie wymagają poprawy.

W zależności od witryny interakcje mogą być rzadkie lub w ogóle ich nie ma. Przykładem mogą być strony, na których znajdują się głównie tekst i obrazy, a interaktywnych elementów jest niewiele lub wcale. W przypadku witryn takich jak edytory tekstu czy gry wiążą się z setkami, a nawet tysiącami interakcji. W obu przypadkach, gdy INP jest wysoki, komfort użytkownika może być zagrożony.

Ulepszenie INP wymaga czasu i wysiłku, ale nagroda zapewnia lepsze wrażenia użytkowników. W tym przewodniku omówimy sposoby na poprawę wartości INP.

Ustalanie przyczyny niskiej wartości INP

Zanim naprawisz problemy z powolnym wczytywaniem, musisz uzyskać dane, które pozwolą Ci określić, czy INP Twojej witryny jest zły czy wymaga poprawy. Gdy już je uzyskasz, możesz przejść do laboratorium, aby rozpocząć diagnostykę powolnego działania interakcji i znaleźć rozwiązanie.

Znajdowanie wolnych interakcji w polu

Optymalizacja INP powinna się zacząć od danych pochodzących z pola. Dzięki danym pól od dostawcy monitorowania użytkowników rzeczywistych (RUM) masz nie tylko wartość INP strony, ale też dane kontekstowe, które wskazują, która konkretnie interakcja była odpowiedzialna za samą wartość INP, czy miała miejsce podczas wczytywania strony czy po niej, jaki był jej typ (kliknięcie, naciśnięcie lub dotknięcie) i inne cenne informacje.

Jeśli nie korzystasz z usług dostawcy RUM, aby pozyskiwać dane z pól, w przewodniku po danych z pól w INP znajdziesz zalecenie używania raportu na temat użytkowania Chrome (CrUX) w usłudze PageSpeed Insights, aby uzupełnić luki w danych. CrUX to oficjalny zbiór danych programu Core Web Vitals, który zawiera ogólne podsumowanie danych dla milionów witryn, w tym INP. Jednak CrUX często nie udostępnia danych kontekstowych, które dostawca RUM udostępnia, aby pomóc w analizowaniu problemów. Dlatego nadal zalecamy, aby strony korzystały w miarę możliwości z usług dostawcy RUM lub wdrożyły własne rozwiązanie RUM, które uzupełnia to, co jest dostępne w CrUX.

Diagnozowanie powolnych interakcji w module

Najlepiej zacząć testowanie w laboratorium, gdy masz już dane z pola, które wskazują na wolne interakcje. W przypadku braku danych z pola możesz zastosować w laboratorium kilka strategii identyfikowania powolnych interakcji. Takie strategie obejmują śledzenie typowych ścieżek użytkowników i testowanie interakcji na poszczególnych etapach, a także interakcje ze stroną podczas wczytywania (gdy główny wątek jest często najbardziej obciążony), aby wykrywać powolne interakcje w tym kluczowym momencie ścieżki użytkownika.

Optymalizacja interakcji

Gdy zidentyfikujesz powolną interakcję i będziesz w stanie ręcznie odtworzyć ją 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ę wraz z wywołaniem interakcji zwrotnej zdarzenia.
  2. Czas przetwarzania, który obejmuje czas potrzebny na wywołanie funkcji wywołania zwrotnego zdarzenia.
  3. Opóźnienie prezentacji, czyli czas potrzebny przeglądarce na zaprezentowanie następnej klatki, która zawiera wizualny rezultat interakcji.

Suma tych 3 faz to całkowity czas oczekiwania na interakcję. Każdy etap interakcji przekłada się na całkowite opóźnienie interakcji, dlatego musisz wiedzieć, jak ją zoptymalizować, aby trwała jak najkrócej.

Wykrywanie i zmniejszanie opóźnienia danych wejściowych

Gdy użytkownik wchodzi w interakcję ze stroną, pierwszą częścią tej interakcji jest opóźnienie przy pierwszym działaniu. W zależności od innych działań na stronie opóźnienia w przyjmowaniu danych mogą być znaczne. Może to być spowodowane aktywnością w głównym wątku (np. wczytywaniem, analizowaniem i kompilowaniem skryptów), obsługą pobierania, funkcjami timera lub nawet innymi interakcjami, które występują szybko po sobie i nakładają się na siebie.

Niezależnie od źródła opóźnienia wejścia interakcji, warto je zminimalizować, aby interakcje mogły jak najszybciej wywoływać funkcje zwrotne zdarzeń.

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

Kluczowym aspektem interakcji w cyklu życia strony jest czas uruchamiania. Podczas wczytywania strony najpierw jest ona renderowana, ale należy pamiętać, że renderowanie nie oznacza, że wczytywanie zostało zakończone. W zależności od tego, ile zasobów wymaga strona, aby działać w pełni, użytkownicy mogą próbować wchodzić z nią w interakcję, gdy jest ona jeszcze wczytywania.

Opóźnienie w wejściu danych podczas wczytywania strony może wydłużać czas interakcji, na przykład ze względu na interpretację skryptu. Po pobraniu pliku JavaScript z sieci przeglądarka musi jeszcze wykonać kilka czynności, zanim kod JavaScript będzie mógł zostać uruchomiony. Do tych czynności należy zanalizowanie skryptu w celu sprawdzenia poprawności jego składni, skompilowanie go w bajtkod i ostatecznie jego wykonanie.

W zależności od rozmiaru skryptu może to spowodować długie wykonywanie zadań w wątku głównym, co opóźnia reagowanie przeglądarki na inne interakcje użytkownika. Aby strona była responsywna na działania użytkownika podczas wczytywania, musisz wiedzieć, jak zmniejszyć prawdopodobieństwo długiego wykonywania zadań podczas wczytywania, aby strona była szybka.

Optymalizacja wywołań zwrotnych zdarzenia

Opóźnienie danych wejściowych to tylko pierwsza część pomiaru INP. Musisz też dopilnować, by wywołania zwrotne zdarzeń uruchamiane w odpowiedzi na interakcję użytkownika były jak najszybciej realizowane.

Często udziela się w głównym wątku.

Ogólną radą przy optymalizacji wywołań zwrotnych zdarzeń jest wykonanie w nich jak najmniejszej pracy. Logika interakcji może jednak być skomplikowana i możesz tylko nieznacznie zmniejszyć ilość pracy wykonywanej przez te procesy.

Jeśli tak jest w przypadku Twojej witryny, możesz spróbować podzielić zadania w event callback na osobne zadania. Zapobiega to temu, aby zbiorcza praca nie stała się długim zadaniem, które blokuje wątek główny. Dzięki temu inne interakcje, które normalnie czekałyby na wątek główny, mogą się uruchomić wcześniej.

setTimeout to jeden ze sposobów dzielenia zadań, ponieważ przekazywana do niego funkcja wywołania zwrotnego jest wykonywana w ramach nowego zadania. Możesz używać samej funkcji setTimeout lub abstrakcyjnie ją wykorzystać w osobnej funkcji, aby uzyskać większą ergonomię.

Bezwzględne oddawanie wątku jest lepsze niż nieoddawanie go wcale. Istnieje jednak bardziej wyrafinowany sposób oddawania wątku głównemu, który polega na oddawaniu go natychmiast po wywołaniu zwrotnym zdarzenia, które aktualizuje interfejs użytkownika, aby logika renderowania mogła działać wcześniej.

Pozwól na szybsze rozpoczęcie pracy z renderowaniem

Bardziej zaawansowana technika generowania zysków obejmuje opracowanie struktury kodu w wywołaniach zwrotnych zdarzeń w celu ograniczenia przebiegu procesu do logiki wymaganej do zastosowania aktualizacji wizualnych w następnej klatce. Pozostałe zadania można odłożyć na później. Dzięki temu wywołania zwrotne nie tylko są szybkie i sprawne, ale też skracają czas renderowania interakcji, ponieważ nie pozwalają na blokowanie aktualizacji wizualnych w kodzie wywołania zwrotnego zdarzenia.

Wyobraź sobie na przykład edytor tekstu sformatowanego, który formatuje tekst podczas pisania i aktualizuje inne aspekty interfejsu w zależności od tego, co piszesz (np. liczba słów, wyróżnianie błędów pisowni i inne ważne informacje wizualne). Ponadto aplikacja może być zmuszona do zapisania wprowadzonej treści, aby w przypadku jej zamknięcia i powrotu nie utracić żadnej pracy.

W tym przykładzie w odpowiedzi na wpisy użytkownika muszą wystąpić 4 działania. Jednak zanim pojawi się kolejna klatka, wystarczy zrobić pierwszy element.

  1. Zaktualizuj pole tekstowe, wpisując w nim tekst użytkownika i zastosuj odpowiednie formatowanie.
  2. Zaktualizuj część interfejsu, w której wyświetla się bieżąca liczba słów.
  3. Uruchom funkcję logiczną, aby sprawdzić pisownię.
  4. Zapisz najnowsze zmiany (lokalnie lub w zdalnej bazie danych).

Kod, który to umożliwia, 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 opóźnienie nieistotnych aktualizacji do następnej klatki może skrócić czas przetwarzania, a w konsekwencji zmniejszyć ogólny czas oczekiwania na interakcję.

Ilustracja przedstawiająca interakcję z klawiaturą i kolejne czynności w 2 scenariuszach. Na górnym rysunku zadanie renderowania i wszystkie kolejne zadania w tle są wykonywane synchronicznie, dopóki nie nadejdzie czas na wyświetlenie klatki. Na dolnym rysunku najpierw wykonywane są zadania krytyczne dla renderowania, a potem oddaje się kontrolę nad wątkiem głównym, aby szybciej wyświetlić nowy kadr. Następnie są wykonywane zadania w tle.
Kliknij ten rysunek, aby zobaczyć wersję w wysokiej rozdzielczości.

Użycie w poprzednim przykładzie kodu funkcji setTimeout() wewnątrz wywołania requestAnimationFrame() jest co prawda nieco ezoteryczne, ale jest to skuteczna metoda, która działa we wszystkich przeglądarkach i zapewnia, że kod niekrytyczny nie blokuje następnego kadru.

Unikanie thrashingu układu

Zbyt częste układy – czasami nazywane wymuszaniem synchronicznego układu – to problem z wydajnością podczas renderowania, gdy układ jest tworzony synchronicznie. Dzieje się tak, gdy zaktualizujesz style w JavaScripcie, a następnie odczytasz je w tym samym zadaniu. Istnieje wiele właściwości w JavaScripcie, które mogą powodować thrashing układu.

Wizualizacja miksowania układu w panelu wydajności w Narzędziach deweloperskich w Chrome.
Przyklad chaotycznego układu, jak pokazano w panelu wydajności w Narzędziach deweloperskich w Chrome. Zadania renderowania, które wiążą się z przeładowaniem układu, będą oznaczone czerwonym trójkątem w prawym górnym rogu części stosu wywołania. Często będą też opatrzone etykietą Ponownie oblicz styl lub Układ.

Zmiana układu jest wąskim gardłem wydajności, ponieważ aktualizowanie stylów i natychmiastowe żądanie wartości tych stylów w JavaScriptzie zmusza przeglądarkę do synchronicznego wykonywania zadań związanych z układem, które można by wykonać asynchronicznie w późniejszym czasie, gdy zakończą działanie funkcje zwrotne zdarzeń.

Minimalizowanie opóźnienia prezentacji

Opóźnienie wyświetlania interakcji obejmuje czas od zakończenia wywołania zwrotnego zdarzenia interakcji do momentu, w którym przeglądarka jest w stanie narysować następny kadr, który pokazuje wynikowe zmiany wizualne.

Minimalizowanie rozmiaru DOM

Jeśli DOM strony jest mały, renderowanie zwykle kończy się szybko. Jednak gdy DOM-y stają się bardzo duże, renderowanie ma tendencję do skalowania wraz ze wzrostem rozmiaru DOM-u. Związek między pracą związaną z renderowaniem a rozmiarem DOM nie jest liniowy, ale renderowanie dużych DOM-ów wymaga więcej pracy niż małych. Duży DOM stanowi problem w 2 przypadkach:

  1. Podczas wstępnego renderowania strony, gdy duży DOM wymaga dużej pracy, by wyrenderować początkowy stan strony.
  2. W reakcji na interakcję użytkownika, gdy duży DOM może powodować bardzo kosztowne renderowanie aktualizacji, a w konsekwencji wydłużać czas potrzebny przeglądarce na wyświetlenie kolejnego kadru.

Pamiętaj, że w niektórych przypadkach duże DOM nie można znacznie ograniczyć. Istnieją sposoby na zmniejszenie rozmiaru modelu DOM, np. spłaszczenie modelu DOM lub dodawanie do modelu DOM podczas interakcji z użytkownikiem, aby zachować mały rozmiar początkowy modelu DOM, ale te techniki mogą nie wystarczyć.

Użyj elementu content-visibility, aby leniwie renderować elementy poza ekranem.

Jednym ze sposobów ograniczenia czasochłonnego renderowania zarówno podczas wczytywania strony, jak i renderowania w odpowiedzi na interakcje użytkowników jest odwoływanie się do właściwości CSS content-visibility, która w efekcie oznacza leniwe renderowanie elementów zbliżających się do widocznego obszaru. Chociaż content-visibility wymaga wprawy, warto sprawdzić, czy dzięki niemu skróci się czas renderowania, co może poprawić INP strony.

Koszt wydajności podczas renderowania kodu HTML za pomocą JavaScriptu

Gdzie jest HTML, tam jest analizowanie HTML. Gdy przeglądarka zakończy analizowanie HTML do modelu DOM, musi zastosować do niego style, wykonać obliczenia układu, a następnie go wyrenderować. Jest to nieunikniony koszt, ale ma znaczenie sposób renderowania kodu HTML.

Gdy serwer wysyła kod HTML, trafia on do przeglądarki w postaci strumienia. Strumieniowanie oznacza, że odpowiedź HTML z serwera jest dostarczana w kawałkach. Przeglądarka optymalizuje sposób obsługi strumienia, stopniowo analizując jego fragmenty w miarę ich pojawiania się i renderując je po kawałku. Jest to optymalizacja wydajności, ponieważ przeglądarka domyślnie generuje przychody okresowo i automatycznie podczas wczytywania strony, a do tego bezpłatnie.

Podczas pierwszej wizyty w dowolnej witrynie zawsze jest pewna ilość kodu HTML, ale zwykle zaczyna się od minimalnej ilości kodu HTML, a do wypełniania obszaru treści służy JavaScript. Kolejne aktualizacje tego obszaru treści również są wynikiem interakcji użytkowników. Jest to zwykle nazywane modelem aplikacji jednostronicowej (SPA). Jednym z zalet tego wzorca jest to, że renderowanie kodu HTML za pomocą JavaScriptu po stronie klienta nie tylko wiąże się z obciążeniem związanym z przetworzeniem kodu JavaScript na kod HTML, ale też z tym, że przeglądarka nie zwróci wyniku, dopóki nie zakończy analizowania i renderowania kodu HTML.

Pamiętaj jednak, że nawet w przypadku witryn, które nie są SPA, prawdopodobnie dochodzi do renderowania kodu HTML za pomocą JavaScriptu w wyniku interakcji. Jest to zwykle normalne, o ile nie renderujesz na kliencie dużych ilości kodu HTML, ponieważ może to opóźnić prezentację następnej klatki. Należy jednak pamiętać, że to podejście do renderowania kodu HTML w przeglądarce może mieć wpływ na wydajność i wpływać na responsywność witryny na działania użytkowników, jeśli renderujesz dużo kodu HTML za pomocą JavaScriptu.

Podsumowanie

Ulepszanie INP witryny to proces iteracyjny. Gdy naprawisz w polu wolne działanie interakcji, istnieje duże prawdopodobieństwo, że znajdziesz inne wolno działające interakcje, zwłaszcza jeśli Twoja witryna jest bardzo interaktywna. Będziesz musiał je też zoptymalizować.

Kluczem do poprawy INP jest trwałość. Z czasem możesz doprowadzić do tego, że użytkownicy będą zadowoleni z wygody korzystania ze strony. Jest też spora szansa, że podczas tworzenia nowych funkcji dla użytkowników będziesz musiał przejść przez ten sam proces optymalizacji interakcji z nimi. Zajmie to trochę czasu i wysiłku, ale warto.

Obraz główny z Unsplash autorstwa Davida Pisnoy, zmodyfikowany zgodnie z licencją Unsplash.