Widzę, że nie blokuj wątku głównego. i „podzielić długie zadania”, Ale co to znaczy robić te rzeczy?
Typowe porady dotyczące szybkiego działania aplikacji JavaScript sprowadzają się do następujących wskazówek:
- „Nie blokuj wątku głównego”.
- „Podziel długie zadania”.
To dobra wskazówka, ale jak wygląda praca w niej? JavaScript bez dostawy jest dobry, ale czy to automatycznie przekłada się na bardziej elastyczne interfejsy użytkownika? Być może, ale nie.
Aby zrozumieć, jak optymalizować zadania w języku JavaScript, musisz wiedzieć, czym są zadania i jak przeglądarka je obsługuje.
Co to jest zadanie?
Zadanie to dowolne pojedyncze zadanie wykonywane przez przeglądarkę. Obejmuje to renderowanie, analizowanie kodu HTML i CSS, uruchamianie JavaScriptu oraz inne działania, nad którymi możesz nie mieć bezpośredniej kontroli. Spośród wszystkich tych elementów napisany przez Ciebie JavaScript jest prawdopodobnie największym źródłem zadań.
![Wizualizacja zadania na przykład w narzędziu do zarządzania wydajnością w Narzędziach deweloperskich w Chrome. Zadanie znajduje się u góry stosu. Zawiera moduł obsługi zdarzeń kliknięcia, wywołanie funkcji i inne elementy poniżej. Zadanie obejmuje też zadania renderowania po prawej stronie.](https://web.dev/static/articles/optimize-long-tasks/image/a-screenshot-a-task-dep-d13d0ab0d87a7.png?authuser=4&hl=pl)
click
w Narzędziach deweloperskich w Chrome narzędzie do profilowania wydajności.
Zadania powiązane z JavaScriptem wpływają na wydajność na kilka sposobów:
- Gdy przeglądarka pobiera plik JavaScript podczas uruchamiania, umieszcza w kolejce zadania do przeanalizowania i skompilowania, by możliwe było jego późniejsze wykonanie.
- Czasami po otwarciu strony zadania są umieszczane w kolejce, gdy działa JavaScript. Dotyczy to na przykład wywoływania interakcji za pomocą modułów obsługi zdarzeń, animacji opartych na języku JavaScript i aktywności w tle, np. zbierania danych analitycznych.
Wszystkie te rzeczy – z wyjątkiem instancji roboczych i podobnych interfejsów API – odbywają się w wątku głównym.
Jaki jest główny wątek?
Wątek główny to miejsce, w którym większość zadań jest uruchamiana w przeglądarce i w którym wykonuje się prawie cały napisany przez Ciebie JavaScript.
Wątek główny może przetwarzać tylko 1 zadanie naraz. Każde zadanie, które trwa dłużej niż 50 milisekund, jest długim zadaniem. W przypadku zadań dłuższych niż 50 milisekund całkowity czas działania zadania pomniejszony o 50 milisekund jest nazywany okresem blokowania zadania.
Przeglądarka blokuje interakcje podczas wykonywania zadania o dowolnej długości, ale nie jest to zauważalne dla użytkownika, dopóki zadania nie trwają zbyt długo. Jednak gdy użytkownik będzie próbował wejść w interakcję z wieloma długimi zadaniami, interfejs nie odpowiada, a nawet może przestać działać, jeśli wątek główny będzie zablokowany na dłuższy czas.
![To długie zadanie w narzędziu profilowania wydajności w Narzędziach deweloperskich w Chrome. Blokująca część zadania (dłuższa niż 50 milisekund) jest przedstawiona za pomocą czerwonych ukośnych pasów.](https://web.dev/static/articles/optimize-long-tasks/image/a-long-task-the-performa-7ee6e4867ad3d.png?authuser=4&hl=pl)
Aby uniknąć zbyt długiego blokowania wątku głównego, możesz podzielić długie zadanie na kilka mniejszych.
![Jedno długie zadanie i to samo zadanie podzielone na krótsze zadania. Długie zadanie to jeden duży prostokąt, a zadanie podzielone na fragmenty to pięć mniejszych pól o takiej samej szerokości jak długie zadanie.](https://web.dev/static/articles/optimize-long-tasks/image/a-single-long-task-versus-724bb5ecd4b3f.png?authuser=4&hl=pl)
Ma to znaczenie, ponieważ gdy zadania są podzielone, przeglądarka może reagować na działania o wyższym priorytecie znacznie szybciej – także na interakcje użytkowników. Później pozostałe zadania są wykonywane do końca, dzięki czemu zadania umieszczone na początku kolejki zostały wykonane.
![Ilustracja pokazująca, jak podzielenie zadania może ułatwić użytkownikowi interakcję. U góry długie zadanie blokuje działanie modułu obsługi zdarzeń do momentu jego ukończenia. Zadanie posegmentowane na dole umożliwia działanie modułu obsługi zdarzeń szybciej, niż byłoby to w Twoim przypadku.](https://web.dev/static/articles/optimize-long-tasks/image/a-depiction-how-breaking-999bc2dd02872.png?authuser=4&hl=pl)
U góry powyższej liczby widać, że moduł obsługi zdarzeń umieszczony w kolejce przez interakcję użytkownika musiał czekać na rozpoczęcie długiego zadania. Powoduje to opóźnienia w realizacji interakcji. W takim przypadku użytkownik mógł zauważyć opóźnienie. U dołu moduł obsługi zdarzeń może zacząć działać wcześniej, a interakcja może wyglądać natychmiast.
Skoro już wiesz, dlaczego dzielenie zadań jest tak ważne, dowiedz się, jak to robić w języku JavaScript.
Strategie zarządzania zadaniami
W architekturze oprogramowania zaleca się, aby podzielić swoją pracę na mniejsze obszary:
function saveSettings () {
validateForm();
showSpinner();
saveToDatabase();
updateUI();
sendAnalytics();
}
W tym przykładzie jest to funkcja o nazwie saveSettings()
, która wywołuje 5 funkcji w celu weryfikacji formularza, wyświetlenia wskaźnika postępu, wysyłania danych do backendu aplikacji, aktualizacji interfejsu użytkownika i wysyłania statystyk.
Zasadniczo saveSettings()
to dobrze architektura. Jeśli musisz debugować jedną z tych funkcji, możesz przejrzeć drzewo projektu, aby dowiedzieć się, jak działa każda z nich. Taki podział ułatwia nawigowanie w projektach i ich obsługę.
Potencjalny problem może jednak polegać na tym, że JavaScript nie uruchamia każdej z tych funkcji jako osobnych zadań, ponieważ są one wykonywane w ramach funkcji saveSettings()
. Oznacza to, że wszystkie 5 funkcji będzie działać jako jedno zadanie.
![funkcja saveSettings przedstawiona w narzędziu do profilowania wydajności Chrome. Podczas gdy funkcja najwyższego poziomu wywołuje 5 innych funkcji, cała praca odbywa się w ramach jednego długiego zadania, które blokuje wątek główny.](https://web.dev/static/articles/optimize-long-tasks/image/the-savesettings-function-b71e8e42d8bf7.png?authuser=4&hl=pl)
saveSettings()
, która wywołuje pięć funkcji. Zadanie jest uruchamiane w ramach jednego długiego zadania monolitycznego.
W najlepszym przypadku nawet jedna z tych funkcji może wydłużyć czas trwania zadania o co najmniej 50 milisekund. W najgorszym przypadku większość z tych zadań może działać znacznie dłużej – zwłaszcza na urządzeniach z ograniczonymi zasobami.
Ręczne odroczenie wykonania kodu
Jedna z metod używanych przez deweloperów do dzielenia zadań na mniejsze wymaga setTimeout()
. W przypadku tej metody przekazujesz funkcję do setTimeout()
. Opóźnia to wykonanie wywołania zwrotnego do osobnego zadania, nawet jeśli ustawisz limit czasu 0
.
function saveSettings () {
// Do critical work that is user-visible:
validateForm();
showSpinner();
updateUI();
// Defer work that isn't user-visible to a separate task:
setTimeout(() => {
saveToDatabase();
sendAnalytics();
}, 0);
}
Jest to tzw. zysk i najlepiej sprawdza się w przypadku serii funkcji, które muszą działać sekwencyjnie.
Pamiętaj jednak, że kod nie zawsze jest zorganizowany w ten sposób. Możliwe na przykład, że masz do przetworzenia dużą ilość danych w pętli, a to zadanie może zająć bardzo dużo czasu w przypadku wielu iteracji.
function processData () {
for (const item of largeDataArray) {
// Process the individual item here.
}
}
Używanie narzędzia setTimeout()
w tym przypadku jest problematyczne ze względu na ergonomię programistów, a przetworzenie całej tablicy danych może zająć bardzo dużo czasu, nawet jeśli każda iteracja przebiega szybko. Wszystko się sumuje, a setTimeout()
nie jest odpowiednim narzędziem do tego celu – przynajmniej w takim przypadku.
Aby utworzyć punkty zysku, użyj wartości async
/await
Aby ważne zadania widoczne dla użytkownika były wykonywane przed zadaniami o niższym priorytecie, możesz przejść do wątku głównego, przerywając na chwilę kolejkę zadań, które umożliwiają uruchamianie ważniejszych zadań w przeglądarce.
Jak wyjaśniliśmy wcześniej, uprawnienia setTimeout
można wykorzystać, aby przejść do wątku głównego. Dla wygody i poprawia czytelności możesz jednak wywołać funkcję setTimeout
w elemencie Promise
i przekazać jej metodę resolve
jako wywołanie zwrotne.
function yieldToMain () {
return new Promise(resolve => {
setTimeout(resolve, 0);
});
}
Zaletą funkcji yieldToMain()
jest to, że możesz ją await
w dowolnej funkcji async
. Nawiązując do poprzedniego przykładu, możesz utworzyć tablicę funkcji do uruchomienia i uzyskać dostęp do wątku głównego po każdej z nich:
async function saveSettings () {
// Create an array of functions to run:
const tasks = [
validateForm,
showSpinner,
saveToDatabase,
updateUI,
sendAnalytics
]
// Loop over the tasks:
while (tasks.length > 0) {
// Shift the first task off the tasks array:
const task = tasks.shift();
// Run the task:
task();
// Yield to the main thread:
await yieldToMain();
}
}
W rezultacie zadanie, które dotychczas było monolityczne, zostało podzielone na osobne zadania.
![Ta sama funkcja SaveSettings, która jest dostępna w narzędziu do profilowania wydajności Chrome, tylko z uzyskaniem wyników. W efekcie dotychczasowe zadanie monolityczne zostało podzielone na 5 zadań – po jednym dla każdej funkcji.](https://web.dev/static/articles/optimize-long-tasks/image/the-same-savesettings-fun-689035655ea7a.png?authuser=4&hl=pl)
saveSettings()
wykonuje teraz funkcje podrzędne jako osobne zadania.
Dedykowany interfejs API algorytmu szeregowania
setTimeout
to skuteczny sposób dzielenia zadań, ale ma też wadę: gdy przejdziesz do wątku głównego przez odroczenie kodu do wykonania w kolejnym zadaniu, to zadanie zostanie dodane na koniec kolejki.
Jeśli kontrolujesz cały kod na stronie, możesz utworzyć własny algorytm szeregowania z możliwością ustalania priorytetów zadań, ale skrypty innych firm nie będą z niego korzystać. W takiej sytuacji nie możesz ustalać priorytetów pracy w takich środowiskach. Możesz je tylko dzielić na fragmenty lub bezpośrednio odpowiadać interakcjom użytkownika.
Interfejs API algorytmu szeregowania udostępnia funkcję postTask()
, która umożliwia bardziej precyzyjne planowanie zadań, i jest jednym ze sposobów na umożliwienie przeglądarce nadawania priorytetów pracom, tak aby zadania o niskim priorytecie trafiały do wątku głównego. postTask()
korzysta z obietnic i akceptuje jedno z 3 ustawień priority
:
'background'
dla zadań o najniższym priorytecie.'user-visible'
w przypadku zadań o średnim priorytecie. Jest to wartość domyślna, jeśli nie ustawiono żadnego elementupriority
.'user-blocking'
w przypadku krytycznych zadań, które muszą być uruchamiane z wysokim priorytetem.
Oto przykładowy kod, w którym interfejs API postTask()
służy do uruchamiania 3 zadań o najwyższym możliwym priorytecie, a pozostałe 2 zadania o najniższym możliwym priorytecie.
function saveSettings () {
// Validate the form at high priority
scheduler.postTask(validateForm, {priority: 'user-blocking'});
// Show the spinner at high priority:
scheduler.postTask(showSpinner, {priority: 'user-blocking'});
// Update the database in the background:
scheduler.postTask(saveToDatabase, {priority: 'background'});
// Update the user interface at high priority:
scheduler.postTask(updateUI, {priority: 'user-blocking'});
// Send analytics data in the background:
scheduler.postTask(sendAnalytics, {priority: 'background'});
};
W tym przypadku priorytety zadań są planowane w taki sposób, że zadania priorytetowe w przeglądarce (np. interakcje z użytkownikiem) mogą być realizowane w miarę potrzeb.
![funkcja saveSettings przedstawiona w narzędziu do profilowania wydajności Chrome, ale przy użyciu postTask. postTask dzieli każdą funkcję SaveSettings, nadając im priorytety, tak aby interakcja użytkownika była możliwa bez blokowania.](https://web.dev/static/articles/optimize-long-tasks/image/the-savesettings-function-5f4625a39191a.png?authuser=4&hl=pl)
saveSettings()
funkcja planuje poszczególne funkcje za pomocą postTask()
. Kluczowe zadania wykonywane przez użytkowników są zaplanowane z wysokim priorytetem, natomiast zadania, o których użytkownik nie wie, są zaplanowane w tle. Dzięki temu interakcje z użytkownikiem mogą być realizowane szybciej, ponieważ praca jest podzielona i nadająca odpowiedni priorytet.
To tylko uproszczony przykład użycia pola postTask()
. Można tworzyć wystąpienia różnych obiektów TaskController
, które mogą mieć wspólne priorytety między zadaniami. Obejmuje to możliwość zmiany priorytetów różnych instancji TaskController
w razie potrzeby.
Wbudowany zysk z kontynuacją przy użyciu nadchodzącego interfejsu API scheduler.yield()
Jedną z proponowanych nowości do interfejsu API algorytmu szeregowania jest scheduler.yield()
– interfejs API zaprojektowany specjalnie w celu generowania zysków do głównego wątku w przeglądarce. Przypomina funkcję yieldToMain()
omówioną wcześniej w tym przewodniku:
async function saveSettings () {
// Create an array of functions to run:
const tasks = [
validateForm,
showSpinner,
saveToDatabase,
updateUI,
sendAnalytics
]
// Loop over the tasks:
while (tasks.length > 0) {
// Shift the first task off the tasks array:
const task = tasks.shift();
// Run the task:
task();
// Yield to the main thread with the scheduler
// API's own yielding mechanism:
await scheduler.yield();
}
}
Ten kod jest dobrze znany, ale zamiast yieldToMain()
wykorzystuje on
await scheduler.yield()
![Trzy diagramy przedstawiające zadania bez generowania, zysku oraz zysku i kontynuacji. W przeciwnym razie trzeba czekać na długie zadania. Dzięki generowaniu wyników jest więcej zadań, które są krótsze, ale mogą je zakłócać inne, niepowiązane zadania. Przy generowaniu i kontynuacji jest więcej zadań, które są krótsze, ale kolejność ich wykonywania jest zachowywana.](https://web.dev/static/articles/optimize-long-tasks/image/three-diagrams-depicting-13b4f9ac49a85.png?authuser=4&hl=pl)
scheduler.yield()
, wykonywanie zadania zaczyna się w miejscu, w którym zostało przerwane, nawet po punkcie zysku.
Zaletą funkcji scheduler.yield()
jest kontynuacja, co oznacza, że jeśli wykonasz zadanie w środku zestawu zadań, pozostałe zaplanowane zadania będą kontynuowane w tej samej kolejności po punkcie zysku. Dzięki temu kod skryptów zewnętrznych nie zakłóca kolejności wykonywania kodu.
Użycie polecenia scheduler.postTask()
w połączeniu z zasadą priority: 'user-blocking'
również wiąże się z wysokim prawdopodobieństwem dotyczącym kontynuacji ze względu na wysoki priorytet user-blocking
. Tymczasem możesz stosować tę metodę jako alternatywę.
Użycie zasady setTimeout()
(lub scheduler.postTask()
z zasadą priority: 'user-visibile'
albo brak jawnie zdefiniowanej zasady priority
) powoduje zaplanowanie zadania z tyłu kolejki, dzięki czemu umożliwia uruchomienie innych oczekujących zadań przed kontynuacją.
Nie używaj isInputPending()
Obsługa przeglądarek
- 87
- 87
- x
- x
Interfejs API isInputPending()
umożliwia sprawdzenie, czy użytkownik próbował wejść w interakcję ze stroną, i uzyskuje go tylko w przypadku oczekiwania na odpowiedź.
Dzięki temu JavaScript może kontynuować, gdy żadne dane wejściowe nie oczekują na działanie, zamiast generować i kończyć na końcu kolejki zadań. Może to znacząco poprawić wydajność, zgodnie z opisem w zamianie wysyłki, w przypadku stron, które w przeciwnym razie nie wróciłyby do głównego wątku.
Jednak od czasu wprowadzenia tego interfejsu API nasza wiedza na temat zysków wzrosła, zwłaszcza po wprowadzeniu INP. Nie zalecamy już korzystania z tego interfejsu API. Zamiast tego zalecamy rekomendowanie niezależnie od tego, czy dane wejściowe oczekują, czy nie. Istnieje kilka powodów:
isInputPending()
może nieprawidłowo zwrócić wartośćfalse
, mimo że użytkownik w pewnych okolicznościach wchodził w interakcję z reklamą.- Dane wejściowe to nie jedyny przypadek, w którym zadania powinny przynieść wyniki. Animacje i inne regularne aktualizacje interfejsu użytkownika mogą być równie ważne jak w przypadku elastycznych stron internetowych.
- Od tego czasu wprowadziliśmy bardziej wszechstronne interfejsy API do generowania przychodów, które rozwiązują problemy związane z pozyskiwaniem zasobów, takie jak
scheduler.postTask()
ischeduler.yield()
.
Podsumowanie
Zarządzanie zadaniami nie jest łatwe, ale dzięki temu strona szybciej reaguje na interakcje użytkowników. Nie istnieje jedna konkretna rada dotycząca zarządzania zadaniami i określania ich priorytetów, a jedynie szereg różnych technik. Przypomnijmy: podczas zarządzania zadaniami warto wziąć pod uwagę główne kwestie:
- Oddać wątek główny w przypadku kluczowych zadań dla użytkowników.
- Nadawaj priorytet zadaniom za pomocą funkcji
postTask()
. - Poeksperymentuj z usługą
scheduler.yield()
. - Na koniec wykonuj jak najwięcej pracy w funkcjach.
Co najmniej jedno z tych narzędzi pozwala uporządkować pracę w aplikacji w taki sposób, aby traktowała priorytetowo potrzeby użytkownika, a jednocześnie zapewniła, że mniej ważna praca jest wykonywana. Zwiększy to wygodę użytkowników, którzy będą mogli szybciej i wygodniej korzystać z reklam.
Specjalne podziękowania za kontrolę techniczną tego przewodnika Philipowi Waltonowi.
Miniatura pochodząca z kanału Unsplash, który udostępnił(a) Amirali Mirhashemian.