Powszechnie dostępne porady dotyczące przyspieszenia działania aplikacji JavaScript to między innymi „Nie blokuj wątku głównego” i „Nie blokuj długich zadań”. Na tej stronie wyjaśniamy, co oznaczają te porady i dlaczego optymalizacja zadań w języku JavaScript jest ważna.
Co to jest zadanie?
Zadanie to osobna praca wykonywana przez przeglądarkę. Obejmuje to renderowanie, analizowanie kodu HTML i CSS, uruchamianie napisanego przez Ciebie kodu JavaScript oraz inne działania, nad którymi możesz nie mieć bezpośredniej kontroli. Kod JavaScript na Twoich stronach to główne źródło zadań przeglądarki.
Zadania wpływają na wydajność na kilka sposobów. Jeśli np. przeglądarka pobiera plik JavaScript podczas uruchamiania, umieszcza zadania w kolejce do przeanalizowania i skompilowania, tak aby można było go wykonać. Na późniejszym etapie cyklu życia strony inne zadania rozpoczynają się, gdy już działa JavaScript, np. kierowanie interakcji z użyciem modułów obsługi zdarzeń, animacje z JavaScriptu i działania w tle, np. zbieranie danych analitycznych. Wszystko to z wyjątkiem instancji roboczych i podobnych interfejsów API odbywa się w wątku głównym.
Jaki jest wątek główny?
Wątek główny to miejsce, w którym większość zadań jest uruchamiana w przeglądarce i wykonywana jest prawie cały kod 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 liczone jako długie zadanie. Jeśli użytkownik spróbuje wejść w interakcję ze stroną podczas długiego zadania lub aktualizacji renderowania, przeglądarka musi poczekać na obsługę tej interakcji, co spowoduje opóźnienie.
Aby temu zapobiec, podziel każde długie zadanie na mniejsze zadania, których wykonanie trwa krócej. Jest to tzw. podzielenie długich zadań.
Podział zadań daje przeglądarce więcej możliwości reagowania na zadania o wyższym priorytecie, w tym na interakcje użytkowników między innymi zadaniami. Dzięki temu interakcje będą przebiegać znacznie szybciej, a użytkownik mógł zauważyć opóźnienie w oczekiwaniu na zakończenie długiego zadania.
Strategie zarządzania zadaniami
JavaScript traktuje każdą funkcję jako jedno zadanie, ponieważ wykorzystuje model wykonywania zadań od uruchomienia do zakończenia. Oznacza to, że funkcja wywołująca wiele innych funkcji (np. z przykładu poniżej) musi działać, dopóki nie zostaną one ukończone, co spowalnia przeglądarkę:
function saveSettings () { //This is a long task.
validateForm();
showSpinner();
saveToDatabase();
updateUI();
sendAnalytics();
}
Jeśli Twój kod zawiera funkcje, które wywołują wiele metod, podziel go na kilka funkcji. Dzięki temu przeglądarka nie tylko ma więcej możliwości reagowania na interakcje, ale też ułatwia odczytywanie, obsługę i pisanie testów na potrzeby testów. W sekcjach poniżej omawiamy niektóre strategie podziału długich funkcji i nadawania priorytetów zadaniom, które się z nimi składają.
Ręczne opóźnienie wykonania kodu
Możesz opóźnić wykonanie niektórych zadań, przekazując odpowiednią funkcję do interfejsu setTimeout()
. Będzie to działać nawet wtedy, gdy określisz limit czasu wynoszący 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);
}
Najlepiej sprawdza się to w przypadku szeregu funkcji, które trzeba uruchomić w określonej kolejności. Inaczej jest ustrukturyzowany kod. Następnym przykładem jest funkcja, która przetwarza dużą ilość danych za pomocą pętli. Im większy zbiór danych, tym dłużej potrwa ten proces. Niekoniecznie trzeba umieścić w pętli odpowiednie miejsce na setTimeout()
:
function processData () {
for (const item of largeDataArray) {
// Process the individual item here.
}
}
Na szczęście jest kilka innych interfejsów API, które pozwalają odłożyć wykonanie kodu do późniejszego zadania. Aby skrócić czas oczekiwania, zalecamy używanie postMessage()
.
Możesz też przerwać pracę za pomocą funkcji requestIdleCallback()
, ale zadania planowane są o najniższym priorytecie i tylko podczas bezczynności przeglądarki. Oznacza to, że jeśli wątek główny jest szczególnie zajęty, zadania zaplanowane w requestIdleCallback()
mogą nigdy nie zostać uruchomione.
Użyj async
/await
do utworzenia punktów zysku
Aby mieć pewność, że ważne zadania dla użytkowników będą wykonywane przed zadaniami o niższym priorytecie, zacznij korzystać z wątku głównego przez krótkie przerwanie kolejki zadań, aby przeglądarka mogła uruchomić ważniejsze zadania.
Najprostszy sposób to Promise
, który kończy się wywołaniem funkcji setTimeout()
:
function yieldToMain () {
return new Promise(resolve => {
setTimeout(resolve, 0);
});
}
W funkcji saveSettings()
możesz przejść do wątku głównego po każdym kroku, jeśli await
wywołasz funkcję yieldToMain()
po każdym jej wywołaniu. W ten sposób możesz podzielić długie zadanie na kilka zadań:
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();
}
}
Co ważne, nie musisz wykonywać każdego wywołania funkcji. Jeśli np. uruchamiasz 2 funkcje, które wymagają krytycznych aktualizacji interfejsu, prawdopodobnie lepiej nie korzystać z nich. Jeśli to możliwe, uruchom tę pracę najpierw, a potem rozważ przejście na funkcje wykonywane w tle lub mniej ważne zadania, których użytkownik nie widzi.
Specjalny interfejs API algorytmu szeregowania
Wymienione dotychczas interfejsy API mogą pomóc w rozbiciu zadań, ale mają one istotną wadę: gdy uruchamiasz wątek główny przez odroczenie kodu do uruchomienia w późniejszym zadaniu, kod jest dodawany na końcu kolejki zadań.
Jeśli kontrolujesz cały kod na swojej stronie, możesz utworzyć własny algorytm szeregowania, aby nadać priorytet zadaniom. Skrypty innych firm nie będą jednak używać Twojego algorytmu szeregowania, więc w takim przypadku nie możesz nadawać priorytetów pracy. Możesz je rozdzielić lub pozwolić tylko na interakcje użytkowników.
Interfejs API algorytmu szeregowania udostępnia funkcję postTask()
, która umożliwia bardziej precyzyjne planowanie zadań i pomaga przeglądarce nadawać priorytety pracy, tak aby zadania o niskim priorytecie były wykonywane w wątku głównym. postTask()
korzysta z obietnic i akceptuje ustawienie priority
.
Interfejs postTask()
API ma 3 priorytety:
'background'
dla zadań o najniższym priorytecie.'user-visible'
dla zadań o średnim priorytecie. Jest to ustawienie domyślne, jeśli nie ustawiono żadnej funkcjipriority
.'user-blocking'
na potrzeby krytycznych zadań, które muszą być wykonywane z wysokim priorytetem.
Ten przykładowy kod korzysta z interfejsu API postTask()
do uruchamiania 3 zadań o najwyższym możliwym priorytecie, a pozostałe 2 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 miejscu priorytety zadań są planowane tak, aby zadania priorytetowe w przeglądarce, takie jak interakcje z użytkownikami, mogły działać poprawnie.
Możesz też tworzyć wystąpienia różnych obiektów TaskController
, które mają wspólne priorytety między zadaniami, w tym możliwość zmiany priorytetów różnych instancji TaskController
w razie potrzeby.
Wbudowany zysk z możliwością kontynuacji dzięki nowemu interfejsowi API scheduler.yield()
Co ważne, aby dowiedzieć się więcej o funkcji scheduler.yield()
, poczytaj o jej badaniu dotyczącym origin (od momentu zakończenia) oraz o jego wyjaśnieniu.
Jednym z proponowanych sposobów dodania do interfejsu API algorytmu szeregowania jest scheduler.yield()
, interfejs API zaprojektowany specjalnie pod kątem generowania ruchu do głównego wątku w przeglądarce. Sposób użycia przypomina funkcję yieldToMain()
przedstawioną wcześniej na tej stronie:
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 w dużej mierze znany, ale zamiast yieldToMain()
stosuje się w nim await scheduler.yield()
.
Zaletą funkcji scheduler.yield()
jest kontynuacja, co oznacza, że jeśli wykonasz zadanie w trakcie zestawu zadań, po osiągnięciu punktu zysku pozostałe zaplanowane zadania będą kontynuowane w tej samej kolejności. Dzięki temu skrypty innych firm nie przejmą kontroli nad kolejnością wykonywania kodu.
Używanie scheduler.postTask()
w połączeniu z priority: 'user-blocking'
ma duże prawdopodobieństwo kontynuacji ze względu na wysoki priorytet user-blocking
. Możesz więc używać tej alternatywy, dopóki scheduler.yield()
nie stanie się szerzej dostępny.
Użycie funkcji setTimeout()
(lub scheduler.postTask()
z priority: 'user-visible'
lub bez jawnego priority
) planuje zadanie z tyłu kolejki, umożliwiając uruchamianie innych oczekujących zadań przed kontynuacją.
Zysk na podstawie danych wejściowych z elementem 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 zyskuje tylko wtedy, gdy dane wejściowe oczekują na odpowiedź.
Dzięki temu JavaScript będzie kontynuował, jeśli żadne dane wejściowe nie będą oczekujące, zamiast znikać i kierować się na koniec kolejki zadań. Może to znacznie zwiększyć skuteczność (zgodnie z opisem w zamiarze wysyłki) w przypadku witryn, które w przeciwnym razie nie zwracałyby się do wątku głównego.
Jednak od czasu wprowadzenia tego interfejsu API nasza wiedza na temat zysków wzrosła, zwłaszcza po wprowadzeniu INP. Nie zalecamy już używania tego interfejsu API. Zamiast tego zalecamy generowanie niezależnie od tego, czy dane wejściowe są oczekujące, czy nie. Ta zmiana w rekomendacjach ma kilka przyczyn:
- W niektórych przypadkach, gdy użytkownik wszedł w interakcję z reklamą, interfejs API może nieprawidłowo zwrócić wartość
false
. - Dane wejściowe nie muszą być jedynym przypadkiem, w którym powinny się pojawić zadania. Animacje i inne regularne aktualizacje interfejsu mogą być równie ważne dla stworzenia elastycznej strony internetowej.
- Od tego czasu wprowadzono bardziej wszechstronne interfejsy API, takie jak
scheduler.postTask()
ischeduler.yield()
, aby rozwiązać problemy z zyskami.
Podsumowanie
Zarządzanie zadaniami jest trudne, ale dzięki temu Twoja strona będzie szybciej reagować na interakcje użytkowników. Jest wiele technik zarządzania zadaniami i określania ich priorytetów w zależności od przypadku użycia. Przypomnę, że zarządzając zadaniami, musisz pamiętać o tych kwestiach:
- Udzielanie wątkom głównym zadań o znaczeniu krytycznym, które są widoczne dla użytkownika.
- Rozważ eksperymentowanie z:
scheduler.yield()
. - Ustal priorytety zadań za pomocą aplikacji
postTask()
. - I wreszcie, rób jak najmniejszą pracę nad funkcjami.
Co najmniej 1 z tych narzędzi pozwoli Ci uporządkować zadania w aplikacji w taki sposób, aby priorytetowo traktowały potrzeby użytkownika, a jednocześnie zapewniły możliwość wykonania mniej istotnych czynności. Zwiększa to wygodę użytkowników, dzięki czemu jest bardziej responsywny i wygodniejszy w użyciu.
Specjalne podziękowania dla Philipa Waltona za sprawdzenie techniczne tego dokumentu.
Miniatura pochodzi ze strony Unsplash – Amirali Mirhashemian.