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. Ostateczna wartość INP to najdłuższa zaobserwowana interakcja (czasami z pominięciem wartości odstających).

Aby zapewnić użytkownikom wygodę, witryny powinny mieć czas od interakcji do następnego wyrenderowania wynoszący 200 milisekund lub mniej. Aby mieć pewność, że osiągasz ten cel w przypadku większości użytkowników, warto mierzyć 50. percentyl wczytywania stron w przypadku urządzeń mobilnych i komputerów.

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 może być ich setki, a nawet tysiące. W obu przypadkach, gdy wskaźnik INP jest wysoki, komfort użytkownika może być zagrożony.

Poprawa INP wymaga czasu i wysiłku, ale nagrodą jest lepsze wrażenia użytkownika. W tym przewodniku omówimy sposób na poprawę INP.

Ustalanie przyczyny niskiej wartości INP

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

Znajdowanie interakcji o niskiej szybkości

Optymalizacja INP powinna się zacząć od danych pochodzących z pola. W najlepszym przypadku dane z pola od dostawcy usługi RUM (Real User Monitoring) zawierają nie tylko wartość INP strony, ale też dane kontekstowe, które wskazują, która konkretna interakcja odpowiada za wartość INP, czy interakcja nastąpiła podczas wczytywania strony czy po nim, typ interakcji (kliknięcie, naciśnięcie klawisza lub dotknięcie) oraz 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 laboratorium

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 mieć możliwość jej ręcznego odtworzenia w laboratorium, możesz ją zoptymalizować. Interakcje można podzielić na 3 fazy:

  1. Opóźnienie przy pierwszym działaniu, które rozpoczyna się, gdy użytkownik rozpoczyna interakcję ze stroną, a kończy, gdy zaczynają się wywołania zwrotne zdarzeń związane z tą interakcją.
  2. Czas przetwarzania, który obejmuje czas potrzebny na wywołanie funkcji wywołania zwrotnego zdarzenia.
  3. Opóźnienie wyświetlania, czyli czas potrzebny przeglądarce na wyświetlenie kolejnego klatki zawierającej wizualny wynik interakcji.

Suma tych 3 faz to całkowity czas oczekiwania na interakcję. Każda faza interakcji przyczynia się do opóźnienia interakcji, dlatego ważne jest, aby wiedzieć, jak zoptymalizować każdą część interakcji, 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 wywołujące zdarzenia.

Związek między obliczaniem 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 wprowadzania 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 przy pierwszym działaniu to tylko pierwsza część pomiaru INP. Musisz też zadbać o to, aby funkcje wywoływane powiązane ze zdarzeniami w odpowiedzi na interakcje użytkownika były wykonywane tak szybko, jak to możliwe.

często wracać do głównego wątku;

Najlepszą ogólną radą dotyczącą optymalizacji wywołań zwrotnych zdarzeń jest to, aby wykonywać w nich jak najmniej pracy. Logikę interakcji możesz jednak uznać za skomplikowaną i być może, że będziesz w stanie tylko nieznacznie ograniczyć ich pracę.

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żyć funkcji setTimeout samodzielnie lub umieścić ją w ramach osobnej funkcji , aby uzyskać bardziej ergonomiczny kod.

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.

Yield, aby umożliwić szybsze renderowanie

Bardziej zaawansowana technika rezygnacji polega na ustrukturyzowaniu kodu w funkcjach zwracanych przez zdarzenia w taki sposób, aby ograniczyć wykonywane operacje do logiki wymaganej do zastosowania wizualnych zmian w kolejnych klatkach. Pozostałe zadania można odłożyć na później. Dzięki temu funkcje wywołania są lekkie i szybkie, a czas renderowania interakcji jest krótszy, ponieważ nie pozwalają one na blokowanie aktualizacji wizualnych przez kod wywołania zdarzenia.

Wyobraź sobie na przykład edytor tekstu, który formatuje tekst podczas pisania, ale także aktualizuje inne aspekty interfejsu w odpowiedzi na to, co zostało napisane (np. zliczanie słów, wyróżnianie błędów ortograficznych i inne ważne informacje wizualne). Aplikacja może też potrzebować zapisać to, co zostało napisane, aby w razie opuszczenia i powrotu nie stracić żadnych danych.

W tym przykładzie w odpowiedzi na wpisy użytkownika muszą wystąpić 4 działania. Jednak przed wyświetleniem następnego klatki musi zostać wykonana tylko pierwsza czynność.

  1. Zaktualizuj pole tekstowe, wpisując w nim tekst użytkownika i zastosuj odpowiednie formatowanie.
  2. Zaktualizuj część interfejsu, która wyświetla bieżącą liczbę słów.
  3. Uruchom logikę, aby sprawdzić, czy nie ma literówek.
  4. Zapisywanie najnowszych zmian (lokalnie lub w zdalnej bazie danych).

Kod, który to umożliwia, może wyglądać 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 krytyczne dla 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 powyższy obraz, aby wyświetlić 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.

Unikaj niepotrzebnego przeładowania 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. Występuje, gdy aktualizujesz style w JavaScript, a potem odczytujesz je w tym samym zadaniu. W JavaScript jest wiele właściwości, które mogą powodować twarde przeformatowanie układu.

Wizualizacja twardego układu w panelu wydajności w Narzędziach deweloperskich w Chrome
Przyklad chaotycznego układu, jak widać 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ą się wywołania 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 zazwyczaj kończy się szybko. Jednak gdy DOM-y stają się bardzo duże, renderowanie ma tendencję do skalowania wraz ze wzrostem rozmiaru DOM. 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 jest problematyczny w 2 przypadkach:

  1. Podczas początkowego renderowania strony, gdy duży DOM wymaga dużo pracy, aby 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żych DOM-ów nie można znacząco zmniejszyć. Istnieją pewne metody, które pozwalają zmniejszyć rozmiar 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. Jednak te techniki mogą nie wystarczyć.

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

Jednym ze sposobów na ograniczenie ilości pracy związanej z renderowaniem podczas wczytywania strony i pracy związanej z renderowaniem w odpowiedzi na interakcje użytkownika jest korzystanie z właściwości CSS content-visibility, która polega na opóźnionym renderowaniu elementów w miarę ich pojawiania się w widocznym obszarze. Chociaż content-visibility wymaga trochę 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 sposób renderowania kodu HTML ma znaczenie.

Gdy serwer wysyła kod HTML, trafia on do przeglądarki jako strumień. 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, która polega na tym, że przeglądarka okresowo i automatycznie wczytuje strony, a Ty nie musisz za to płacić.

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ż mają miejsce w wyniku interakcji użytkowników. Jest to zwykle nazywane modelem aplikacji jednostronicowej (SPA). Jednym z mankamentów tego wzorca jest to, że renderowanie kodu HTML za pomocą JavaScriptu po stronie klienta wiąże się nie tylko z kosztem przetwarzania JavaScriptu na potrzeby tworzenia kodu 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 zazwyczaj w porządku, o ile nie renderujesz dużych ilości kodu HTML po stronie klienta, co może opóźnić prezentację kolejnego obrazu. Należy jednak pamiętać, że takie 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żytkownika, jeśli renderujesz dużo kodu HTML za pomocą JavaScriptu.

Podsumowanie

Ulepszanie INP witryny to proces iteracyjny. Gdy naprawisz problem z powolną interakcją w polu, istnieje duże prawdopodobieństwo, że – zwłaszcza jeśli Twoja witryna jest bardzo interaktywna – zaczniesz znajdować inne powolne interakcje, które również będziesz musiał zoptymalizować.

Kluczem do poprawy INP jest wytrwał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 opracowywania 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.