Od interakcji do kolejnego wyrenderowania (INP)

1. Wprowadzenie

Interaktywna wersja demonstracyjna i ćwiczenia z programowania, które pomogą Ci dowiedzieć się więcej o interakcji do kolejnego wyrenderowania (INP).

Diagram przedstawiający interakcję w wątku głównym. Użytkownik wprowadza dane wejściowe, gdy zadania są blokowane. Dane wejściowe są opóźniane do czasu zakończenia tych zadań. Następnie uruchamiane są detektory zdarzeń pointerup, mouseup i zdarzenia kliknięcia, a potem rozpoczyna się renderowanie i malowanie, aż do wyświetlenia następnej klatki.

Wymagania wstępne

  • Znajomość języków HTML i JavaScript.
  • Zalecane: zapoznaj się z dokumentacją INP.

Czego się dowiesz

  • Jak interakcje użytkowników i sposób, w jaki je obsługujesz, wpływają na responsywność strony.
  • Jak ograniczyć i wyeliminować opóźnienia, aby zapewnić użytkownikom komfortowe korzystanie z witryny.

Wymagania

  • Komputer z możliwością klonowania kodu z GitHuba i uruchamiania poleceń npm.
  • edytor tekstu,
  • najnowszej wersji Chrome, aby wszystkie pomiary interakcji działały prawidłowo;

2. Konfiguracja

Pobieranie i uruchamianie kodu

Kod znajdziesz w repozytorium web-vitals-codelabs.

  1. Sklonuj repozytorium w terminalu: 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 znajduje się licznik Wynik i przycisk Zwiększ. Klasyczna demonstracja reaktywności i responsywności.

Zrzut ekranu aplikacji w wersji demonstracyjnej do tych ćwiczeń z programowania

Pod przyciskiem znajdują się 4 pomiary:

  • INP: bieżący wynik INP, który zwykle jest najgorszą interakcją.
  • Interakcja: ocena ostatniej interakcji.
  • FPS: klatki na sekundę w głównym wątku strony.
  • Licznik czasu: animacja licznika czasu, która pomaga wizualizować zacinanie się.

Wpisy FPS i Timer nie są w ogóle potrzebne do pomiaru interakcji. Zostały one dodane tylko po to, aby ułatwić wizualizację responsywności.

Wypróbuj

Spróbuj kliknąć przycisk Zwiększ i obserwuj, jak rośnie wynik. Czy wartości INPInterakcja zmieniają się z każdym przyrostem?

INP mierzy czas od momentu interakcji użytkownika do momentu, w którym strona wyświetla mu wyrenderowaną aktualizację.

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

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

Przejdź do panelu Skuteczność, którego będziesz używać do pomiaru interakcji.

Zrzut ekranu panelu Wydajność w Narzędziach deweloperskich obok aplikacji

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

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

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

Animacja pokazująca, jak nagrać interakcję za pomocą panelu wydajności w Narzędziach deweloperskich

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

Zrzut ekranu panelu Wydajność w Narzędziach deweloperskich. Kursor wskazuje interakcję w panelu, a etykietka wyświetla krótki czas trwania interakcji.

Po najechaniu kursorem na interakcję zobaczysz, że była ona szybka, nie trwała długo (czas przetwarzania) i zajęła minimalny czas (opóźnienie wejściaopóźnienie wyświetlania). Dokładne długości tych opóźnień zależą od szybkości Twojego urządzenia.

4. Detektory zdarzeń działające przez długi czas

Otwórz plik index.js i odkomentuj funkcję 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 zauważalnie wolniejsze.

Ślad wydajności

Nagraj kolejny film w panelu Wydajność, aby zobaczyć, jak to wygląda.

Jednosekundowa interakcja w panelu Wydajność

To, co kiedyś było krótką interakcją, teraz zajmuje całą sekundę.

Gdy najedziesz kursorem na interakcję, zauważysz, że czas jest prawie w całości poświęcony na „Czas przetwarzania”, czyli czas potrzebny na wykonanie wywołań zwrotnych detektora zdarzeń. Ponieważ blokujące wywołanie blockFor odbywa się w całości w detektorze zdarzeń, to tam upływa czas.

5. Eksperyment: czas przetwarzania

Wypróbuj różne sposoby zmiany kolejności pracy detektora zdarzeń, aby zobaczyć, jak wpływa to na INP.

Najpierw zaktualizuj interfejs

Co się stanie, jeśli zamienisz kolejność wywołań JavaScriptu – najpierw zaktualizujesz interfejs, a potem zablokujesz?

Zobacz pełny kod: ui_first.html

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

Czy interfejs pojawił się wcześniej? Czy kolejność ma wpływ na wyniki INP?

Spróbuj wykonać śledzenie i sprawdzić interakcję, aby zobaczyć, czy występują jakieś różnice.

Oddzielni słuchacze

Co się stanie, jeśli przeniesiesz zadanie do osobnego detektora zdarzeń? Zaktualizuj interfejs w jednym detektorze zdarzeń i zablokuj stronę w innym.

Zobacz pełny kod: two_click.html

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

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

Jak to teraz wygląda w panelu wydajności?

Różne typy zdarzeń

Większość interakcji wywołuje wiele rodzajów zdarzeń, od zdarzeń wskaźnika lub klawiatury po zdarzenia najechania kursorem, skupienia/rozmycia i syntetyczne, takie jak beforechange i beforeinput.

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

Co się stanie, jeśli zmienisz typy zdarzeń w przypadku odbiorników zdarzeń? Na przykład czy chcesz zastąpić jeden z click detektorów zdarzeń detektorem 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ń wywołanie aktualizacji interfejsu?

Zobacz pełny kod: no_ui.html

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

6. Wyniki eksperymentu dotyczącego czasu przetwarzania

Ślad wydajności: najpierw zaktualizuj interfejs

Zobacz pełny kod: ui_first.html

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

Na nagraniu z panelu Skuteczność, które pokazuje kliknięcie przycisku, widać, że wyniki się nie zmieniły. Aktualizacja interfejsu została wywołana przed kodem blokującym, ale przeglądarka zaktualizowała to, co zostało wyrenderowane na ekranie, dopiero po zakończeniu działania detektora zdarzeń. Oznacza to, że interakcja trwała nieco ponad sekundę.

Jednosekundowa interakcja w panelu Wydajność

Ślad wydajności: oddzielne detektory

Zobacz pełny kod: two_click.html

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

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

W tym przypadku także nie ma różnicy w działaniu. Interakcja nadal trwa pełną sekundę.

Jeśli bardzo powiększysz interakcję kliknięcia, zobaczysz, że w wyniku zdarzenia click wywoływane są 2 różne funkcje.

Zgodnie z oczekiwaniami pierwszy krok – aktualizacja interfejsu – przebiega bardzo szybko, a drugi zajmuje całą sekundę. Jednak suma ich efektów powoduje, że użytkownik końcowy doświadcza powolnej interakcji.

Przybliżony widok interakcji trwającej sekundę w tym przykładzie, pokazujący, że pierwsze wywołanie funkcji trwało mniej niż milisekundę

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

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

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

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

Powiększony widok interakcji trwającej sekundę w tym przykładzie, pokazujący, że detektor zdarzenia kliknięcia kończy działanie w mniej niż milisekundę po detektorze zdarzenia pointerup.

Ślad wydajności: brak aktualizacji interfejsu

Zobacz pełny kod: no_ui.html

button.addEventListener('click', () => {
  blockFor(1000);
  // score.incrementAndUpdateUI();
});
  • Wynik się nie aktualizuje, ale strona nadal to robi.
  • Animacje, efekty CSS, domyślne działania komponentów internetowych (wprowadzanie danych w formularzu), wpisywanie tekstu i podświetlanie tekstu są nadal aktualizowane.

W tym przypadku przycisk po kliknięciu przechodzi w stan aktywny i wraca do poprzedniego stanu, co wymaga renderowania przez przeglądarkę, a więc nadal występuje INP.

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

Nagranie panelu Wydajność pokazuje interakcję praktycznie identyczną z poprzednimi.

Jednosekundowa interakcja w panelu Wydajność

Na wynos

Każdy kod uruchomiony w dowolnym detektorze zdarzeń opóźni interakcję.

  • Obejmuje to detektory zarejestrowane w różnych skryptach i platformach lub kodzie biblioteki, który działa w detektorach, np. aktualizację stanu, która wywołuje renderowanie komponentu.
  • Dotyczy to nie tylko Twojego kodu, ale też wszystkich skryptów innych firm.

To częsty problem.

Na koniec: to, że Twój kod nie wywołuje renderowania, nie oznacza, że renderowanie nie będzie czekać na zakończenie działania powolnych detektorów zdarzeń.

7. Eksperyment: opóźnienie wejściowe

A co z długo działającym kodem poza detektorami zdarzeń? Na przykład:

  • Jeśli masz reklamę <script>, która wczytuje się z opóźnieniem i losowo blokuje stronę podczas wczytywania.
  • Wywołanie interfejsu API, np. setInterval, które okresowo blokuje stronę?

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

Zobacz pełny kod: input_delay.html

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


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

Co się dzieje

8. Wyniki eksperymentu dotyczącego opóźnienia wejściowego

Zobacz pełny kod: input_delay.html

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


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

Zarejestrowanie kliknięcia przycisku, które nastąpiło podczas wykonywania zadania blokującego setInterval, spowoduje długotrwałą interakcję, nawet jeśli w samej interakcji nie wykonano żadnej pracy blokującej.

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 przypisywany głównie do opóźnienia wejściowego, a nie do czasu przetwarzania.

Panel Wydajność w Narzędziach deweloperskich pokazujący blokujące zadanie trwające sekundę, interakcję, która następuje w połowie tego zadania, oraz interakcję trwającą 642 milisekundy, w większości przypisaną do opóźnienia wejścia

Pamiętaj, że nie zawsze ma to wpływ na interakcje. Jeśli nie klikniesz podczas wykonywania zadania, możesz mieć szczęście. Takie „losowe” kichnięcia mogą być koszmarem do debugowania, gdy tylko czasami powodują problemy.

Możesz je wykryć, mierząc długie zadania (lub długie klatki animacji) i łączny czas blokowania.

9. Powolna prezentacja

Do tej pory analizowaliśmy wydajność JavaScriptu na podstawie opóźnienia danych wejściowych lub odbiorników zdarzeń. Co jeszcze wpływa na renderowanie następnego malowania?

Aktualizowanie strony za pomocą kosztownych efektów.

Nawet jeśli aktualizacja strony nastąpi szybko, przeglądarka może mieć problem z jej renderowaniem.

W wątku głównym:

  • Frameworki interfejsu, które muszą renderować aktualizacje po zmianach stanu
  • Zmiany w DOM lub przełączanie wielu kosztownych selektorów zapytań CSS może wywołać wiele operacji związanych ze stylem, układem i rysowaniem.

Poza wątkiem głównym:

  • Używanie CSS do obsługi efektów GPU
  • Dodawanie bardzo dużych obrazów w wysokiej rozdzielczości
  • Rysowanie złożonych scen za pomocą SVG/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 przebudowuje cały DOM bez wstrzymywania się w celu zapewnienia wstępnej informacji wizualnej.
  • Strona wyszukiwania, która oferuje złożone filtry wyszukiwania z dynamicznym interfejsem użytkownika, ale do tego celu wykorzystuje kosztowne odbiorniki.
  • przełącznik trybu ciemnego, który wywołuje styl lub układ całej strony;

10. Eksperyment: opóźnienie prezentacji

Wolny nośnik requestAnimationFrame

Symulujmy długie opóźnienie prezentacji za pomocą requestAnimationFrame() interfejsu API.

Przenieś wywołanie blockFor do wywołania zwrotnego requestAnimationFrame, aby było ono wykonywane po zwróceniu detektora zdarzeń:

Zobacz pełny kod: presentation_delay.html

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

Co się dzieje

11. Wyniki eksperymentu dotyczącego opóźnienia prezentacji

Zobacz pełny kod: presentation_delay.html

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

Interakcja trwa nadal sekundę, więc co się stało?

requestAnimationFrame żąda wywołania zwrotnego przed kolejnym renderowaniem. Ponieważ INP mierzy czas od interakcji do kolejnego wyrenderowania, blockFor(1000)requestAnimationFrame nadal blokuje kolejne wyrenderowanie przez pełną sekundę.

Jednosekundowa interakcja w panelu Wydajność

Zwróć jednak uwagę na 2 rzeczy:

  • Po najechaniu kursorem zobaczysz, że cały czas interakcji jest teraz poświęcany na „opóźnienie prezentacji”, ponieważ blokowanie wątku głównego następuje po powrocie z detektora zdarzeń.
  • Głównym elementem aktywności wątku głównego nie jest już zdarzenie kliknięcia, ale „Animation Frame Fired” (Wywołano ramkę animacji).

12. Diagnozowanie interakcji

Na tej stronie testowej responsywność jest bardzo widoczna dzięki wynikom, licznikom i interfejsowi licznika, ale podczas testowania przeciętnej strony jest ona mniej oczywista.

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

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

Na dowolnej stronie możesz użyć Narzędzi deweloperskich, aby zmierzyć jej elastyczność. Aby wyrobić sobie ten nawyk, wykonaj te czynności:

  1. Przeglądaj internet w zwykły sposób.
  2. Obserwuj dziennik interakcji w widoku danych na żywo w panelu Wydajność w Narzędziach deweloperskich.
  3. Jeśli widzisz interakcję o niskiej skuteczności, spróbuj ją powtórzyć:
  • Jeśli nie możesz tego powtórzyć, skorzystaj z dziennika interakcji, aby uzyskać statystyki.
  • Jeśli możesz powtórzyć ten problem, nagraj ślad w panelu Wydajność.

Wszystkie opóźnienia

Spróbuj dodać do strony trochę każdego z tych problemów:

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: praca asynchroniczna

Ponieważ w ramach interakcji możesz uruchamiać efekty niewizualne, takie jak wysyłanie żądań sieciowych, włączanie timerów czy aktualizowanie stanu globalnego, co się stanie, gdy te efekty w końcu zaktualizują stronę?

Dopóki kolejne wyrenderowanie po interakcji może się odbyć, pomiar interakcji jest zatrzymywany, nawet jeśli przeglądarka uzna, że nie potrzebuje nowej aktualizacji renderowania.

Aby to sprawdzić, kontynuuj aktualizowanie interfejsu z poziomu detektora kliknięć, ale uruchamiaj blokujące działanie z poziomu limitu czasu.

Zobacz pełny kod: timeout_100.html

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

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

Co dalej?

14. Wyniki eksperymentu dotyczącego pracy asynchronicznej

Zobacz pełny kod: timeout_100.html

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

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

Interakcja trwająca 27 milisekund z długim zadaniem trwającym sekundę, która teraz występuje później w śladzie

Interakcja jest teraz krótka, ponieważ główny wątek jest dostępny natychmiast po zaktualizowaniu interfejsu. Długie zadanie blokujące nadal działa, ale jest wykonywane jakiś czas po wyrenderowaniu, więc użytkownik od razu otrzymuje informację zwrotną z interfejsu.

Lekcja: jeśli nie możesz go usunąć, przynajmniej go przenieś.

Metody

Czy możemy uzyskać lepszy wynik niż stałe 100 milisekund setTimeout? Prawdopodobnie nadal chcemy, aby kod działał jak najszybciej, w przeciwnym razie po prostu byśmy go usunęli.

Cel:

  • Interakcja zostanie uruchomiona incrementAndUpdateUI().
  • blockFor() zostanie uruchomiony jak najszybciej, ale nie będzie blokować kolejnego renderowania.
  • Dzięki temu zachowanie jest przewidywalne i nie ma „magicznych limitów czasu”.

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

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

„requestPostAnimationFrame”

W przeciwieństwie do samego requestAnimationFrame (który próbuje uruchomić przed kolejnym renderowaniem i zwykle powoduje powolną interakcję) połączenie requestAnimationFrame + setTimeout stanowi prosty kod polyfill dla requestPostAnimationFrame, który uruchamia wywołanie zwrotne po kolejnym renderowaniem.

Zobacz pełny kod: raf+task.html

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

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

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

Dla wygody możesz nawet opakować go w obietnicę:

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 wściekłych kliknięć)

Przesunięcie długotrwałych zadań blokujących może pomóc, ale te długie zadania nadal blokują stronę, co wpływa na przyszłe interakcje, a także na wiele innych animacji i aktualizacji strony.

Spróbuj ponownie wczytać asynchroniczną wersję strony z blokowaniem (lub własną, jeśli w ostatnim kroku udało Ci się opracować własną odmianę odraczania pracy):

Zobacz pełny kod: timeout_100.html

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

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

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

Ślad wydajności

Przy każdym kliknięciu w kolejce pojawia się zadanie trwające sekundę, co zapewnia, że główny wątek jest blokowany przez dłuższy czas.

Wątek główny wykonuje wiele zadań trwających kilka sekund, co powoduje, że interakcje trwają nawet 800 ms.

Gdy te długie zadania nakładają się na nowe kliknięcia, interakcje są powolne, mimo że sam detektor zdarzeń zwraca wynik niemal natychmiast. Stworzyliśmy taką samą sytuację jak w przypadku wcześniejszego eksperymentu z opóźnieniami we wprowadzaniu danych. Tym razem jednak opóźnienie danych wejściowych nie wynika z setInterval, ale z pracy wywołanej przez wcześniejsze detektory zdarzeń.

Strategie

Najlepiej byłoby całkowicie usunąć długie zadania.

  • Całkowicie usuń niepotrzebny kod, zwłaszcza skrypty.
  • Zoptymalizuj kod, aby uniknąć wykonywania długotrwałych zadań.
  • Przerywaj nieaktualne działania, gdy pojawią się nowe interakcje.

16. Strategia 1. Debouncing

Klasyczna strategia. Gdy interakcje następują szybko po sobie, a przetwarzanie lub efekty sieciowe są kosztowne, celowo opóźnij rozpoczęcie pracy, aby móc ją anulować i ponownie uruchomić. Ten wzorzec jest przydatny w interfejsach użytkownika, takich jak pola autouzupełniania.

  • Użyj setTimeout, aby opóźnić rozpoczęcie kosztownej pracy za pomocą timera, np. 500–1000 milisekund.
  • Zapisz identyfikator timera.
  • Jeśli pojawi się nowa interakcja, anuluj poprzedni licznik czasu 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);
});

Ślad wydajności

Wiele interakcji, ale w wyniku wszystkich tych interakcji tylko jedno długie zadanie.

Mimo wielu kliknięć uruchamiane jest tylko jedno zadanie blockFor, które czeka na brak kliknięć przez pełną sekundę. W przypadku interakcji, które występują w seriach – np. wpisywania tekstu lub elementów docelowych, które mają być klikane wielokrotnie i szybko – jest to idealna strategia do stosowania domyślnie.

17. Strategia 2. Przerywanie długotrwałych zadań

Istnieje jednak niewielkie prawdopodobieństwo, że kolejne kliknięcie nastąpi tuż po upływie okresu eliminacji drgań, w trakcie długiego zadania, i z powodu opóźnienia wejściowego stanie się bardzo powolną interakcją.

Jeśli interakcja nastąpi w trakcie wykonywania zadania, chcemy wstrzymać pracę, aby od razu zająć się nowymi interakcjami. Jak to zrobić?

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

Wiele 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);
  });
});

Działa to w ten sposób, że przeglądarka może zaplanować każde zadanie osobno, a dane wejściowe mogą mieć wyższy priorytet.

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

Wracamy do 5 sekund pracy za 5 kliknięć, ale każde 1-sekundowe zadanie na kliknięcie zostało podzielone na 10 zadań trwających 100 milisekund. Dzięki temu nawet w przypadku wielu interakcji nakładających się na te zadania żadna interakcja nie ma opóźnienia we wprowadzaniu danych przekraczającego 100 milisekund. Przeglądarka traktuje priorytetowo odbiorniki zdarzeń przychodzących w stosunku do setTimeout pracy, a interakcje pozostają responsywne.

Ta strategia sprawdza się szczególnie dobrze w przypadku planowania oddzielnych punktów wejścia, np. gdy masz wiele niezależnych funkcji, które musisz wywołać w momencie wczytania aplikacji. Samo wczytywanie skryptów i uruchamianie wszystkiego w momencie oceny skryptu może domyślnie uruchamiać wszystko w ramach jednego długiego zadania.

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

Teraz z yield()

Możemy jednak wykorzystać nowoczesne funkcje asyncawait, aby łatwo dodawać „punkty wydajności” do dowolnej funkcji JavaScript.

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);
});

Jak wcześniej, po wykonaniu części pracy główny wątek jest przekazywany, a przeglądarka może odpowiadać na przychodzące interakcje. Teraz jednak wystarczy użyć await schedulerDotYield() zamiast oddzielnych setTimeout, co sprawia, że jest to wystarczająco wygodne, aby używać go nawet w środku pętli for.

Teraz z AbortContoller()

To działało, ale każda interakcja planowała więcej pracy, nawet jeśli pojawiły się nowe interakcje, które mogły zmienić zakres pracy do wykonania.

W przypadku strategii ograniczania liczby wywołań anulowaliśmy poprzedni limit czasu przy każdej nowej interakcji. Czy możemy zrobić coś podobnego? Możesz to zrobić 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 nastąpi kliknięcie, pętla blockInPiecesYieldyAborty for rozpocznie wykonywanie wszystkich niezbędnych czynności, okresowo zwalniając wątek główny, aby przeglądarka zachowała responsywność w przypadku nowych interakcji.

Gdy nastąpi drugie kliknięcie, pierwsza pętla zostanie oznaczona jako anulowana za pomocą AbortController i rozpocznie się nowa pętla blockInPiecesYieldyAborty. Gdy pierwsza pętla będzie miała zostać ponownie uruchomiona, zauważy, że signal.aborted ma teraz wartość true, i natychmiast zakończy działanie bez wykonywania dalszych czynności.

Praca wątku głównego jest teraz podzielona na wiele małych części, interakcje są krótkie, a praca trwa tylko tak długo, jak jest to konieczne.

18. Podsumowanie

Podzielenie wszystkich długich zadań pozwala witrynie reagować na nowe interakcje. Dzięki temu możesz szybko przekazać wstępne opinie, a także podjąć decyzje, takie jak przerwanie trwającej pracy. Czasami oznacza to zaplanowanie punktów wejścia jako osobnych zadań. Czasami oznacza to dodanie punktów „yield” w odpowiednich miejscach.

Pamiętaj

  • INP mierzy wszystkie interakcje.
  • Każda interakcja jest mierzona od momentu wprowadzenia danych do kolejnego wyrenderowania – w taki sposób użytkownik widzi responsywność.
  • Na czas reakcji na interakcję wpływają opóźnienie wejściowe, czas przetwarzania zdarzenia i opóźnienie prezentacji.
  • Za pomocą Narzędzi deweloperskich możesz łatwo mierzyć INP i szczegółowe informacje o interakcjach.

Strategie

  • Nie umieszczaj na stronach kodu działającego przez długi czas (długich zadań).
  • Przenieś niepotrzebny kod z detektorów zdarzeń do momentu po następnym odświeżeniu.
  • Upewnij się, że aktualizacja renderowania jest wydajna dla przeglądarki.

Więcej informacji