Informacje o tym ćwiczeniu (w Codelabs)
1. Wprowadzenie
Interaktywna wersja demonstracyjna i codelab, które pomogą Ci dowiedzieć się więcej o interakcji do kolejnego wyrenderowania (INP).
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
.
- Sklonuj repozytorium w terminalu:
git clone https://github.com/GoogleChromeLabs/web-vitals-codelabs.git
- Przejdź do sklonowanego katalogu:
cd web-vitals-codelabs/understanding-inp
- Zainstaluj zależności:
npm ci
- Uruchom serwer WWW:
npm run start
- 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.
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 INP i Interakcja zmieniają się z każdym przyrostem?
INP mierzy czas od momentu interakcji użytkownika do momentu, w którym strona wyświetla mu zaktualizowaną wersję.
3. Pomiar interakcji za pomocą Narzędzi deweloperskich w Chrome
Otwórz Narzędzia deweloperskie z menu Więcej narzędzi > Narzędzia dla deweloperó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.
Następnie zarejestruj interakcję w panelu Wydajność.
- Naciśnij przycisk nagrywania.
- Wejdź w interakcję ze stroną (naciśnij przycisk Zwiększ).
- Zatrzymaj nagrywanie.
Na osi czasu zobaczysz ścieżkę Interakcje. Rozwiń go, klikając trójkąt po lewej stronie.
Pojawią się 2 interakcje. Powiększ drugi obraz, przewijając lub przytrzymując klawisz W.
Po najechaniu kursorem na interakcję zobaczysz, że była ona szybka, nie trwała długo (czas przetwarzania) i miała minimalne opóźnienie wejścia i wyświetlania. Dokładne długości tych opóźnień zależą od szybkości Twojego urządzenia.
4. Długotrwałe detektory zdarzeń
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
Zrób kolejne nagranie w panelu Wydajność, aby zobaczyć, jak to wygląda.
To, co kiedyś było krótką interakcją, teraz trwa całą sekundę.
Gdy najedziesz kursorem na interakcję, zauważysz, że prawie cały czas jest poświęcony na „Czas przetwarzania”, czyli czas potrzebny na wykonanie wywołań zwrotnych detektora zdarzeń. Ponieważ wywołanie blokujące 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ąpiły jakieś różnice.
Oddzielni słuchacze
Co się stanie, jeśli przeniesiesz zadanie do osobnego odbiornika 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 detektoró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);
});
Jeśli przyjrzysz się nagraniu z panelu Skuteczność, które pokazuje kliknięcie przycisku, zobaczysz, ż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 narysowane na ekranie, dopiero po zakończeniu działania detektora zdarzeń. Oznacza to, że interakcja trwała nieco ponad sekundę.
Ślad wydajności: oddzielni słuchacze
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 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 tego samego powolnego działania.
Ś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 odbiornik click
, który aktualizuje tylko interfejs, działa teraz po blokującym odbiorniku 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 to oznacza, że nadal występuje INP.
Ponieważ odbiornik zdarzeń zablokował wątek główny na sekundę, uniemożliwiając wyrenderowanie strony, interakcja nadal trwa pełną sekundę.
Nagranie panelu Wydajność pokazuje interakcję niemal identyczną z poprzednimi.
Na wynos
Każdy kod uruchomiony w dowolnym detektorze zdarzeń opóźni interakcję.
- Dotyczy to słuchaczy zarejestrowanych w różnych skryptach i frameworkach lub w kodzie biblioteki, który działa w słuchaczach, np. aktualizacji 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 malowania, nie oznacza, że nie będzie ono czekać na zakończenie działania powolnych odbiornikó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();
});
Nagrywanie kliknięcia przycisku, które nastąpiło podczas wykonywania zadania blokującego setInterval
, powoduje długotrwałą interakcję, nawet jeśli w samej interakcji nie wykonano żadnej pracy blokującej.
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 przypisywany głównie do opóźnienia wejściowego, a nie do czasu przetwarzania.
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 całkowity czas blokowania.
9. Powolna prezentacja
Do tej pory analizowaliśmy wydajność JavaScriptu na podstawie opóźnienia danych wejściowych lub odbiorników zdarzeń, ale 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 głównym wątkiem:
- Używanie CSS do obsługi efektów GPU
- Dodawanie bardzo dużych zdjęć w wysokiej rozdzielczości
- Rysowanie złożonych scen za pomocą SVG/Canvas
Oto kilka przykładów, które często można znaleźć w internecie:
- Witryna SPA, która po kliknięciu linku przebudowuje cały DOM bez wstrzymywania się, aby zapewnić wstępną informację wizualną.
- 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
prosi o oddzwonienie przed kolejnym malowaniem. Ponieważ INP mierzy czas od interakcji do kolejnego wyrenderowania, blockFor(1000)
w requestAnimationFrame
nadal blokuje kolejne wyrenderowanie przez pełną sekundę.
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 funkcji obsługi 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:
- Przeglądaj internet w zwykły sposób.
- Obserwuj dziennik interakcji w widoku danych na żywo w panelu Wydajność w Narzędziach deweloperskich.
- 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ć informacje.
- Jeśli możesz powtórzyć ten problem, nagraj zrzut 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ż efekty niewizualne możesz uruchamiać w ramach interakcji, np. wysyłać żądania sieciowe, włączać timery lub po prostu aktualizować stan globalny, 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 odbiornika 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 jest teraz krótka, ponieważ wątek główny jest dostępny natychmiast po zaktualizowaniu interfejsu. Długie zadanie blokujące nadal działa, ale jest wykonywane jakiś czas po wyrenderowaniu, dzięki czemu 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 wyrenderowaniem i zwykle powoduje powolną interakcję) połączenie requestAnimationFrame
+ setTimeout
stanowi prosty polyfill dla requestPostAnimationFrame
, który uruchamia wywołanie zwrotne po kolejnym wyrenderowaniu.
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.
Gdy te długotrwałe zadania nakładają się na nowe kliknięcia, interakcje są powolne, mimo że sam odbiornik zdarzeń zwraca wynik niemal natychmiast. Stworzyliśmy taką samą sytuację jak w przypadku wcześniejszego eksperymentu z opóźnieniami w danych wejściowych. Tym razem jednak opóźnienie 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
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 odrzucania, w trakcie długotrwałego 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.
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 z nich nie ma opóźnienia we wprowadzaniu danych przekraczającego 100 milisekund. Przeglądarka traktuje priorytetowo przychodzące detektory zdarzeń 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 async
i await
, aby łatwo dodawać „punkty zwrotu” 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 zwalniany, a przeglądarka może odpowiadać na przychodzące interakcje. Teraz jednak wystarczy użyć await schedulerDotYield()
zamiast osobnych setTimeout
, co sprawia, że jest to wystarczająco wygodne, aby używać tej funkcji 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 zadań 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ć 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 nadejdzie kliknięcie, rozpoczyna się pętla blockInPiecesYieldyAborty
for
, która wykonuje wszystkie niezbędne czynności, okresowo zwalniając główny wątek, aby przeglądarka mogła reagować na nowe interakcje.
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.
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, czyli sposobu, w jaki 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, który działa długo (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.