Od interakcji do kolejnego wyrenderowania (INP)

1. Wprowadzenie

Interaktywna prezentacja i ćwiczenia z programowania poświęcone interakcji z następnym wyrenderowaniem (INP).

Schemat przedstawiający interakcję w wątku głównym. Użytkownik wprowadza dane wejściowe, blokując wykonywanie zadań. Dane wejściowe są opóźnione do momentu ukończenia tych zadań. Następnie uruchamiają się detektory zdarzeń wskaźnika, kursora myszy i kliknięcia, a następnie rozpoczynają się renderowanie i malowanie aż do wyświetlenia następnej klatki.

Wymagania wstępne

  • Wiedza z zakresu programowania w językach HTML i JavaScript
  • Zalecane: zapoznaj się z dokumentacją INP.

Czego się nauczysz

  • jak współdziałanie interakcji użytkowników i sposobu obsługi tych interakcji wpływa na responsywność strony.
  • Jak zmniejszyć i wyeliminować opóźnienia, aby zapewnić wygodę użytkownikom.

Wymagania

  • Komputer z możliwością kopiowania kodu z GitHuba i uruchamiania poleceń npm.
  • Edytor tekstu.
  • Najnowsza wersja Chrome umożliwiająca pomiar wszystkich interakcji.

2. Konfiguracja

Pobierz i uruchom kod

Kod znajduje się w repozytorium web-vitals-codelabs.

  1. Skopiuj repozytorium do terminala: git clone https://github.com/GoogleChromeLabs/web-vitals-codelabs.git
  2. Przejdź do sklonowanego katalogu: cd web-vitals-codelabs/understanding-inp
  3. Zainstaluj zależności: npm ci
  4. Uruchom serwer WWW: npm run start
  5. W przeglądarce otwórz stronę http://localhost:5173/understanding-inp/

Omówienie aplikacji

U góry strony znajdują się licznik wyników i przycisk Zwiększaj. Klasyczna wersja demonstracji reaktywności i responsywności.

Zrzut ekranu pokazujący aplikację w wersji demonstracyjnej do tego ćwiczenia w Codelabs

Poniżej przycisku widoczne są cztery pomiary:

  • INP: bieżący wynik INP, który jest zwykle najgorszą interakcją.
  • Interakcja: wynik ostatniej interakcji.
  • Klatki na sekundę: liczba klatek na sekundę głównego wątku.
  • Timer: animacja aktywnego minutnika, która ułatwia wizualizację zacinania.

Pozycje klatek na sekundę i licznika czasu nie są niezbędne do pomiaru interakcji. Są one dodawane tylko po to, aby ułatwić wizualizację reakcji.

Wypróbuj

Spróbuj nacisnąć przycisk Zwiększaj i obserwować wzrost wyniku. Czy wartości INP i Interakcja zmieniają się z każdym przyrostem?

INP określa, ile czasu upływa od momentu interakcji użytkownika z witryną do wyświetlenia użytkownikowi renderowanej aktualizacji.

3. Pomiar interakcji za pomocą Narzędzi deweloperskich w Chrome

Otwórz Narzędzia deweloperskie w sekcji Więcej narzędzi > menu Narzędzia dla deweloperów, klikając stronę prawym przyciskiem myszy i wybierając Zbadaj lub użyj skrótu klawiszowego.

Przejdź do panelu Skuteczność, który służy do pomiaru interakcji.

Zrzut ekranu przedstawiający panel skuteczności Narzędzi deweloperskich obok aplikacji

Następnie zarejestruj interakcję w panelu Skuteczność.

  1. Naciśnij przycisk nagrywania.
  2. Wejdź w interakcję ze stroną (naciśnij przycisk Powiększ).
  3. Zatrzymaj nagrywanie.

Na osi czasu zobaczysz ścieżkę Interakcje. Rozwiń ją, klikając trójkąt po lewej stronie.

Animowana prezentacja rejestrowania interakcji za pomocą panelu skuteczności w Narzędziach deweloperskich

Pojawią się dwie interakcje. Powiększ drugi element, przewijając lub przytrzymując klawisz W.

Zrzut ekranu panelu wydajności w Narzędziach deweloperskich z kursorem najeżdżającym na interakcję w panelu oraz etykietkę z krótkim czasem interakcji.

Po najechaniu kursorem na interakcję zobaczysz, że interakcja była szybka i nie poświęcała czasu na czas przetwarzania, a także minimalny czas opóźnienia danych wejściowych i opóźnienia prezentacji. Dokładny czas trwania będzie zależeć od szybkości komputera.

4. Długo działające detektory zdarzeń

Otwórz plik index.js i usuń znacznik komentarza z funkcji blockFor w detektorze zdarzeń.

Zobacz pełny kod: click_block.html

button.addEventListener('click', () => {
  blockFor(1000);
  score.incrementAndUpdateUI();
});

Zapisz plik. Serwer zobaczy zmianę i odświeży stronę.

Spróbuj ponownie wejść w interakcję ze stroną. Interakcje będą teraz znacznie wolniejsze.

Zrzut wydajności

Zrób jeszcze raz nagranie w panelu Skuteczność, aby zobaczyć, jak to wygląda.

1-sekundowa interakcja w panelu Skuteczność

To, co kiedyś było krótką interakcją, teraz trwa pełną sekundę.

Gdy najedziesz kursorem na interakcję, zwróć uwagę, że cały czas jest prawie w całości określony w kolumnie „Czas przetwarzania”, czyli czas potrzebny na wykonanie wywołań zwrotnych detektora zdarzeń. Blokowanie wywołania blockFor odbywa się w całości w detektorze zdarzeń, więc to właśnie na nim upływa czas.

5. Eksperyment: czas przetwarzania

Wypróbuj sposoby zmiany kolejności zadania detektora zdarzeń, aby zobaczyć, jak wpływa to na wartość INP.

Najpierw zaktualizuj interfejs

Co się stanie, jeśli zmienisz kolejność wywołań js – najpierw zaktualizuj interfejs, a następnie zablokuj go?

Zobacz pełny kod: ui_first.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
  blockFor(1000);
});

Czy zdarzyło Ci się zauważyć, że interfejs pojawił się wcześniej? Czy kolejność wpływa na wyniki INP?

Spróbuj wykonać ślad i przeanalizować interakcję, aby sprawdzić, czy coś się zmieniło.

Osobne detektory

Co się stanie, jeśli przeniesiesz zadanie do innego detektora zdarzeń? Zaktualizować interfejs w jednym detektorze zdarzeń i zablokować stronę w innym detektorze.

Zobacz pełny kod: two_click.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
});

button.addEventListener('click', () => {
  blockFor(1000);
});

Jak teraz wygląda panel Skuteczność?

Różne typy zdarzeń

Większość interakcji wywołuje wiele typów zdarzeń, od wskaźnika lub kluczowych zdarzeń, przez najechanie kursorem, przez zaznaczenie/rozmycie, po zdarzenia syntetyczne, takie jak beforechange i beforeinput.

Wiele prawdziwych stron ma detektory różnych zdarzeń.

Co się stanie, jeśli zmienisz typy zdarzeń dla detektorów zdarzeń? Na przykład zamienić jeden z detektorów zdarzeń click na pointerup lub mouseup?

Zobacz pełny kod: diff_handlers.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
});

button.addEventListener('pointerup', () => {
  blockFor(1000);
});

Brak aktualizacji interfejsu

Co się stanie, jeśli usuniesz z detektora zdarzeń żądanie aktualizacji interfejsu użytkownika?

Zobacz pełny kod: no_ui.html

button.addEventListener('click', () => {
  blockFor(1000);
  // score.incrementAndUpdateUI();
});

6. Wyniki eksperymentu dotyczącego czasu przetwarzania

Śledzenie wydajności: najpierw zaktualizuj interfejs użytkownika

Zobacz pełny kod: ui_first.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
  blockFor(1000);
});

Widać, że w panelu Skuteczność po kliknięciu przycisku widać, że wyniki się nie zmieniły. Aktualizacja interfejsu została aktywowana przed kodem blokującym, ale przeglądarka nie zaktualizowała tego, co zostało wyrenderowane na ekranie, dopóki detektor zdarzeń nie został zakończony. Oznacza to, że do zakończenia interakcji wciąż trwało niecałą sekundę.

Wciąż trwają 1 sekundę w panelu Skuteczność.

Zrzut wydajności: oddzielne detektory

Zobacz pełny kod: two_click.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
});

button.addEventListener('click', () => {
  blockFor(1000);
});

Zasadniczo nie ma różnicy. Interakcja nadal trwa pełną sekundę.

Jeśli powiększysz widok interakcji polegającej na kliknięciu, zobaczysz, że zdarzenie click wywołuje rzeczywiście 2 różne funkcje.

Zgodnie z oczekiwaniami pierwszy, który aktualizuje interfejs, działa niezwykle szybko, a drugi zajmuje całą sekundę. Suma ich efektów skutkuje jednak tym samym powolną interakcją u użytkownika.

Zbliżenie na jednosekundową interakcję w tym przykładzie, które pokazuje, że pierwsze wywołanie funkcji trwa mniej niż milisekundę.

Śledzenie wydajności: różne typy zdarzeń

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
});

button.addEventListener('pointerup', () => {
  blockFor(1000);
});

Te wyniki są bardzo podobne. Interakcja trwa nadal pełną sekundę. jedyna różnica polega na tym, że krótszy detektor click, który służy tylko do aktualizacji UI, działa teraz po blokującym pointerup detektorze.

Przybliżone spojrzenie na jednosekundową interakcję w tym przykładzie: zakończenie działania detektora zdarzeń kliknięcia trwało mniej niż milisekundę po detektorze wskaźnika.

Śledzenie wydajności: bez aktualizacji interfejsu

Zobacz pełny kod: no_ui.html

button.addEventListener('click', () => {
  blockFor(1000);
  // score.incrementAndUpdateUI();
});
  • Wynik nie jest aktualizowany, ale strona nadal tak jest!
  • Animacje, efekty CSS, domyślne działania komponentu internetowego (wpisywanie formularza), wprowadzanie tekstu i wyróżnianie tekstu są w dalszym ciągu aktualizowane.

W takim przypadku przycisk przechodzi w stan aktywny i po kliknięciu cofa się, co wymaga wyrenderowania przez przeglądarkę, co oznacza, że nadal istnieje INP.

Ponieważ detektor zdarzeń zablokował na sekundę wątek główny, co uniemożliwiało wyrenderowanie strony, interakcja nadal trwa pełną sekundę.

Nagranie panelu Skuteczność pokazuje, jaka interakcja była praktycznie taka sama jak wcześniej.

Wciąż trwają 1 sekundę w panelu Skuteczność.

Na wynos

Dowolny kod uruchomiony w dowolnym detektorze zdarzeń opóźni interakcję.

  • Obejmuje to detektory zarejestrowanych przy użyciu różnych skryptów oraz kod platformy lub biblioteki, który działa w detektorach, np. aktualizację stanu uruchamiającą renderowanie komponentu.
  • Nie tylko Twój kod, ale również wszystkie skrypty innych firm.

To częsty problem.

To, że kod nie uruchamia renderowania, nie oznacza, że renderowanie nie będzie czekać na zakończenie powolnych detektorów zdarzeń.

7. Eksperyment: opóźnienie w danych wejściowych

A co z długotrwałym kodem spoza detektorów zdarzeń? Na przykład:

  • Masz zbyt późno wczytywany kod <script>, który losowo zablokował stronę podczas ładowania.
  • Wywołanie interfejsu API, na przykład setInterval, które okresowo blokuje stronę?

Spróbuj usunąć obiekt blockFor z detektora zdarzeń i dodać go do elementu setInterval():

Zobacz pełny kod: input_delay.html

setInterval(() => {
  blockFor(1000);
}, 3000);


button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
});

Co się dzieje

8. Wyniki eksperymentu z opóźnieniem danych wejściowych

Zobacz pełny kod: input_delay.html

setInterval(() => {
  blockFor(1000);
}, 3000);


button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
});

Rejestrowanie kliknięcia przycisku, które ma miejsce, gdy działało zadanie blokujące setInterval, prowadzi do długotrwałej interakcji, nawet jeśli w samej interakcji nie są wykonywane żadne działania blokujące.

Te długotrwałe okresy są często nazywane długimi zadaniami.

Po najechaniu kursorem na interakcję w Narzędziach deweloperskich zobaczysz, że czas interakcji jest teraz przypisany głównie do opóźnienia danych wejściowych, a nie czasu przetwarzania.

Panel wydajności w Narzędziach deweloperskich z widocznym jednosekundowym zadaniem blokującym, interakcją przechodzącą w jego ramach oraz interakcją trwającą 642 milisekundy, głównie przypisywaną opóźnieniu sygnału wejściowego

Pamiętaj, że zawsze nie wpływa to na interakcje. Jeśli nie klikniesz żadnego z nich w trakcie wykonywania zadania, może się okazać, że masz szczęście. Takie „przypadkowe” kichanie może być koszmarem, jeśli tylko czasami powoduje problemy.

Jednym ze sposobów na ich śledzenie jest pomiar długich zadań (lub długich klatek animacji) i całkowitego czasu blokowania.

9. Powolna prezentacja

Do tej pory monitorowaliśmy wydajność JavaScriptu przez opóźnienia danych wejściowych lub detektory zdarzeń, ale co jeszcze wpływa na renderowanie przy następnym wyrenderowaniu?

Cóż, aktualizacja strony przy użyciu drogich efektów!

Nawet jeśli strona aktualizuje się szybko, przeglądarka może mieć problemy z renderowaniem.

W wątku głównym:

  • platformy interfejsu, które muszą renderować aktualizacje po zmianie stanu.
  • Zmiany DOM lub przełączenie wielu kosztownych selektorów zapytań CSS mogą wywołać wiele działań w sekcjach Styl, Układ i Renderowanie.

Poza głównym wątkiem:

  • Używanie CSS do ulepszania efektów GPU
  • dodawanie bardzo dużych zdjęć o wysokiej rozdzielczości,
  • Rysowanie złożonych scen z użyciem SVG lub Canvas

Szkic różnych elementów renderowania w internecie

RenderingNG

Oto kilka przykładów często spotykanych w internecie:

  • Witryna SPA, która po kliknięciu linku odbudowuje cały DOM bez przerw w celu przedstawienia wstępnych informacji wizualnych.
  • Strona wyszukiwania oferująca złożone filtry wyszukiwania z dynamicznym interfejsem użytkownika, ale używająca do tego kosztownych detektorów.
  • Przełącznik trybu ciemnego, który aktywuje styl/układ całej strony

10. Eksperyment: opóźnienie prezentacji

Wolny nośnik requestAnimationFrame

Zrobimy symulację długiego opóźnienia prezentacji za pomocą interfejsu API requestAnimationFrame().

Przenieś wywołanie blockFor do wywołania zwrotnego requestAnimationFrame, aby uruchamiało się po zwróceniu detektora zdarzeń:

Zobacz pełny kod: prezentacja_delay.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
  requestAnimationFrame(() => {
    blockFor(1000);
  });
});

Co się dzieje

11. Wyniki eksperymentu z opóźnieniem prezentacji

Zobacz pełny kod: prezentacja_delay.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();
  requestAnimationFrame(() => {
    blockFor(1000);
  });
});

Interakcja trwa sekundę. Co się stało?

requestAnimationFrame prosi o oddzwonienie przed kolejnym wyrenderowaniem. INP mierzy czas od interakcji do następnego wyrenderowania, więc blockFor(1000) w requestAnimationFrame nadal blokuje następne wyrenderowanie przez pełną sekundę.

Wciąż trwają 1 sekundę w panelu Skuteczność.

Zwróć jednak uwagę na 2 rzeczy:

  • Po najechaniu kursorem zobaczysz cały czas interakcji w kolumnie „Opóźnienie prezentacji”. bo blokowanie wątku głównego następuje po powrocie detektora zdarzeń.
  • Głównym elementem aktywności wątku głównego nie jest już zdarzenie kliknięcia, tylko zdarzenie „Uruchomiono ramkę animacji”.

12. Diagnozowanie interakcji

Na tej stronie testowej czas reagowania jest bardzo spójny – zarówno wyniki, jak i liczniki czasu oraz interfejs licznika – ale przy testowaniu strony przeciętnej jest on nieco bardziej subtelny.

Gdy interakcje trwają długo, nie zawsze jest jasne, co jest przyczyną. Czy to:

  • Opóźnienie wejściowe?
  • Czas przetwarzania wydarzenia?
  • Opóźnienie prezentacji?

Na każdej stronie możesz użyć Narzędzi deweloperskich, aby łatwiej mierzyć czas responsywności. Aby wyrobić w sobie nawyk, spróbuj tak:

  1. Możesz poruszać się po internecie w zwykły sposób.
  2. Opcjonalnie: pozostaw konsolę Narzędzi deweloperskich otwartą, a rozszerzenie Web Vitals rejestruje interakcje.
  3. Jeśli zauważysz nieskuteczną interakcję, powtórz ją:
  • Jeśli nie możesz się powtórzyć, użyj dzienników konsoli, aby uzyskać statystyki.
  • Jeśli możesz go powtórzyć, nagraj to w panelu wyników.

Wszystkie opóźnienia

Spróbuj dodać do strony wszystkie wymienione poniżej problemy:

Zobacz pełny kod: all_the_things.html

setInterval(() => {
  blockFor(1000);
}, 3000);

button.addEventListener('click', () => {
  blockFor(1000);
  score.incrementAndUpdateUI();

  requestAnimationFrame(() => {
    blockFor(1000);
  });
});

Następnie użyj konsoli i panelu wydajności, aby zdiagnozować problemy.

13. Eksperyment: działanie asynchroniczne

Wewnątrz interakcji mogą występować efekty niewizualne, takie jak wysyłanie żądań sieciowych, uruchamianie liczników czasu lub po prostu aktualizowanie stanu globalnego. Co się stanie, gdy w końcu zaktualizują one stronę?

Pomiar interakcji jest zatrzymywany, o ile następne wyrenderowanie po interakcji jest dozwolone, nawet jeśli przeglądarka uzna, że nie wymaga nowej aktualizacji renderowania.

Aby to wypróbować, aktualizuj interfejs przy użyciu detektora kliknięć, ale uruchamiaj blokowanie, gdy minie czas oczekiwania.

Zobacz pełny kod: time_100.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();

  setTimeout(() => {
    blockFor(1000);
  }, 100);
});

Co dalej?

14. Wyniki asynchronicznego eksperymentu służbowego

Zobacz pełny kod: time_100.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();

  setTimeout(() => {
    blockFor(1000);
  }, 100);
});

Interakcja trwająca 27 milisekund z zadaniem o długości 1 sekundy, które występuje w późniejszym czasie logu czasu

Interakcja jest teraz krótka, ponieważ wątek główny jest dostępny od razu po zaktualizowaniu interfejsu. Długie zadanie blokowania nadal działa, tylko jakiś czas po wyrenderowaniu, więc użytkownik natychmiast uzyska informacje zwrotne dotyczące interfejsu.

Lekcja: jeśli nie możesz go usunąć, przynajmniej go przesuwaj!

Metody

Czy mamy coś lepszego niż stały setTimeout w 100 milisekundach? Prawdopodobnie nadal zależy nam, aby kod uruchamiał się jak najszybciej, bo w przeciwnym razie powinniśmy go po prostu usunąć.

Cel:

  • Interakcja zostanie uruchomiona incrementAndUpdateUI().
  • blockFor() uruchomi się tak szybko, jak to możliwe, ale nie zablokuje następnego wyrenderowania.
  • Powoduje to przewidywalne działanie bez „magicznych limitów czasu”.

Możesz to osiągnąć na kilka sposobów:

  • setTimeout(0)
  • Promise.then()
  • requestAnimationFrame
  • requestIdleCallback
  • scheduler.postTask()

„requestPostAnimationFrame”

W przeciwieństwie do samej metody requestAnimationFrame (która będzie próbowała uruchomić się przed następnym wyrenderowaniem i zwykle nadal będzie powodować powolną interakcję), funkcja requestAnimationFrame + setTimeout zapewnia prosty kod Polyfill dla requestPostAnimationFrame, który uruchamia wywołanie zwrotne po następnym wyrenderowaniu.

Zobacz pełny kod: raf+task.html

function afterNextPaint(callback) {
  requestAnimationFrame(() => {
    setTimeout(callback, 0);
  });
}

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();

  afterNextPaint(() => {
    blockFor(1000);
  });
});

Jeśli zależy Ci na ergonomii, możesz to osiągnąć.

Zobacz pełny kod: raf+task2.html

async function nextPaint() {
  return new Promise(resolve => afterNextPaint(resolve));
}

button.addEventListener('click', async () => {
  score.incrementAndUpdateUI();

  await nextPaint();
  blockFor(1000);
});

15. Wiele interakcji (i kliknięć złośliwych)

Długie blokowanie może rozwiązać problem, ale takie długie zadania nadal blokują stronę, co ma wpływ na przyszłe interakcje oraz na wiele innych animacji i aktualizacji strony.

Wypróbuj ponownie roboczą wersję strony z blokowaniem asynchronicznym (lub własną, jeśli w ostatnim kroku udało Ci się opracować własną odmianę odroczenia pracy):

Zobacz pełny kod: time_100.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();

  setTimeout(() => {
    blockFor(1000);
  }, 100);
});

Co się stanie, jeśli szybko klikniesz kilka razy?

Zrzut wydajności

Każde kliknięcie powoduje, że w kolejce pojawia się jednosekundowe zadanie, przez co wątek główny jest zablokowany przez dłuższy czas.

Kilka drugich zadań w wątku głównym, które powodują interakcje już po 800 ms

Gdy te długie zadania nakładają się na nowe kliknięcia, skutkuje to spowolnieniem interakcji, mimo że sam detektor zdarzeń powraca niemal natychmiast. Sytuacja jest taka sama jak we wcześniejszym eksperymencie z opóźnieniami danych wejściowych. Tylko tym razem opóźnienie danych wejściowych nie pochodzi z setInterval, lecz z pracy wyzwalanych przez wcześniejsze detektory zdarzeń.

Strategie

Najlepiej jest całkowicie usunąć długie zadania.

  • Usuń całkowicie zbędny kod – zwłaszcza skrypty.
  • Zoptymalizuj kod, aby uniknąć wykonywania długich zadań.
  • Przerwij nieaktualne zadania po pojawieniu się nowych interakcji.

16. Strategia 1: odbicie

To klasyczna strategia. Jeśli interakcje pojawiają się po krótkim czasie, a ich przetwarzanie lub efekty sieciowe są kosztowne, opóźnij rozpoczęcie ich pracy, by móc je anulować i uruchomić ponownie. Ten wzorzec jest przydatny w interfejsach takich jak pola autouzupełniania.

  • Użyj funkcji setTimeout, aby opóźnić rozpoczęcie kosztownej pracy z minutnikiem, na przykład z 500–1000 milisekund.
  • Następnie zapisz identyfikator minutnika.
  • Jeśli pojawi się nowa interakcja, anuluj poprzedni minutnik za pomocą funkcji clearTimeout.

Zobacz pełny kod: debounce.html

let timer;
button.addEventListener('click', () => {
  score.incrementAndUpdateUI();

  if (timer) {
    clearTimeout(timer);
  }
  timer = setTimeout(() => {
    blockFor(1000);
  }, 1000);
});

Zrzut wydajności

Wiele interakcji, ale tylko jedno długotrwałe zadanie jako wynik ich wszystkich

Pomimo kilku kliknięć tylko jedno zadanie blockFor działa, oczekiwanie na żadne kliknięcia przez pełną sekundę przed uruchomieniem. Ta strategia jest idealna w przypadku interakcji, które występują w seriach, np. wpisywania tekstu lub elementów docelowych, które powinny uzyskiwać wiele szybkich kliknięć.

17. Strategia 2: zakłócić długotrwałą pracę

Istnieje jeszcze prawdopodobieństwo, że dalsze kliknięcie pojawi się tuż po upływie okresu odbicia, trafi w środku długiego zadania i stanie się bardzo wolną interakcją z powodu opóźnienia danych wejściowych.

Warto wstrzymać tę intensywną pracę, gdy interakcja znajduje się w środku naszego zadania. Dzięki temu wszelkie nowe interakcje będą obsługiwane natychmiast. Jak to zrobić?

Istnieją pewne interfejsy API, takie jak isInputPending, ale zwykle lepiej dzielić długie zadania na części.

Wiele elementów (setTimeout)

Pierwsza próba: zrób coś prostego.

Zobacz pełny kod: small_tasks.html

button.addEventListener('click', () => {
  score.incrementAndUpdateUI();

  requestAnimationFrame(() => {
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
    setTimeout(() => blockFor(100), 0);
  });
});

Dzięki temu przeglądarka może planować każde zadanie z osobna, więc dane wejściowe mogą mieć wyższy priorytet.

Wiele interakcji, ale wszystkie zaplanowane zadania zostały podzielone na wiele mniejszych zadań

Wracamy do pełnych pięciu sekund pracy na 5 kliknięć, ale każde jednosekundowe zadanie na kliknięcie zostało podzielone na dziesięć 100-milisekundowych zadań. W efekcie – nawet w przypadku wielu interakcji nakładających się na te zadania – opóźnienie danych wejściowych przekracza 100 milisekund. Przeglądarka nadaje detektorom zdarzeń przychodzących priorytet nad zadaniem setTimeout, a interakcje pozostają elastyczne.

Ta strategia sprawdza się szczególnie dobrze przy planowaniu osobnych punktów wejścia, np. gdy masz wiele niezależnych funkcji, które musisz wywołać podczas wczytywania aplikacji. Samo wczytanie skryptów i uruchomienie wszystkiego w momencie oceny skryptu może domyślnie uruchomić wszystko w wielkim długim zadaniu.

Ta strategia nie sprawdza się jednak w przypadku dzielenia ściśle powiązanego kodu, np. w pętli for, która korzysta ze stanu wspólnego.

Teraz z: yield()

Możemy jednak wykorzystać nowoczesne async i await, aby łatwo dodać „punkty zysku” do dowolnej funkcji JavaScriptu.

Na przykład:

Zobacz pełny kod: Yieldy.html

// Polyfill for scheduler.yield()
async function schedulerDotYield() {
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  });
}

async function blockInPiecesYieldy(ms) {
  const ms_per_part = 10;
  const parts = ms / ms_per_part;
  for (let i = 0; i < parts; i++) {
    await schedulerDotYield();

    blockFor(ms_per_part);
  }
}

button.addEventListener('click', async () => {
  score.incrementAndUpdateUI();
  await blockInPiecesYieldy(1000);
});

Tak jak wcześniej, wątek główny jest generowany po wykonaniu pewnej pracy i przeglądarka jest w stanie reagować na przychodzące interakcje, ale teraz wystarczy tylko await schedulerDotYield(), a nie osobne elementy setTimeout, dzięki czemu jest na tyle ergonomiczny, że można z niego korzystać nawet w pętli for.

Teraz z: AbortContoller()

Udało się, ale każda interakcja pozwala zaplanować więcej pracy, nawet jeśli pojawiły się nowe interakcje i mogły zmienić to, co trzeba zrobić.

Za pomocą strategii odbijania wiadomości anulowaliśmy poprzedni limit czasu przy każdej nowej interakcji. Czy możemy zrobić coś podobnego? Możesz to zrobić na przykład za pomocą AbortController():

Zobacz pełny kod: aborty.html

// Polyfill for scheduler.yield()
async function schedulerDotYield() {
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  });
}

async function blockInPiecesYieldyAborty(ms, signal) {
  const parts = ms / 10;
  for (let i = 0; i < parts; i++) {
    // If AbortController has been asked to stop, abandon the current loop.
    if (signal.aborted) return;

    await schedulerDotYield();

    blockFor(10);
  }
}

let abortController = new AbortController();

button.addEventListener('click', async () => {
  score.incrementAndUpdateUI();

  abortController.abort();
  abortController = new AbortController();

  await blockInPiecesYieldyAborty(1000, abortController.signal);
});

Gdy użytkownik klika reklamę, uruchamia pętlę blockInPiecesYieldyAborty for, która wykonuje niezbędne zadania. Jednocześnie okresowo generuje wątek główny, aby przeglądarka mogła reagować na nowe interakcje.

Po drugim kliknięciu pierwsza pętla zostaje oznaczona jako anulowana za pomocą parametru AbortController i uruchamiana jest nowa pętla blockInPiecesYieldyAborty. Przy następnym zaplanowaniu ponownego uruchomienia pierwszej pętli widz zauważa, że signal.aborted ma teraz wartość true i natychmiast powraca bez wykonywania dalszych działań.

Główny wątek pracy jest teraz podzielony na wiele małych elementów, interakcje są krótkie i trwają tylko tak długo, jak to konieczne.

18. Podsumowanie

Podzielenie wszystkich długich zadań umożliwia witrynie reagowanie na nowe interakcje. Dzięki temu możesz szybko przekazać wstępne uwagi i podejmować decyzje, np. przerwać pracę w toku. Czasami oznacza to planowanie punktów wejścia jako osobnych zadań. Czasami oznacza to dodanie „zysku” w dogodnych punktach.

Pamiętaj

  • INP mierzy wszystkie interakcje.
  • Każda interakcja jest mierzona od wejścia do kolejnego wyrenderowania – sposobu widzenia reakcji użytkownika.
  • Opóźnienie reakcji, czas przetwarzania zdarzenia i opóźnienie prezentacji wszystkie wpływają na responsywność interakcji.
  • W Narzędziach deweloperskich możesz łatwo przeprowadzać pomiary INP i interakcji.

Strategie

  • nie masz na stronach długo działającego kodu (długich zadań).
  • Przenieś zbędny kod z detektorów zdarzeń do czasu następnego wyrenderowania.
  • Zadbaj o wydajność procesu renderowania w przeglądarce.

Więcej informacji