setTimeout: optimalizace dlouhých úloh v JS
Metoda s použitím funkce setTimeout()
slouží k optimalizaci dlouhých úloh v JavaScriptu, které blokují uživatelské interakce a zhoršují tím metriky jako Total Blocking Time (TBT) nebo Interaction to Next Paint (INP), která je důležitou součástí sady Core Web Vitals.
Problém? Dlouhé úlohy v JavaScriptu
JavaScript v prohlížeči pracuje na principu jednoho vlákna. V praxi to znamená, že všechny akce, události a interakce se řadí do systémů front, které ovládá mechanismus zvaný event loop.
Z pohledu optimalizace rychlosti webu nastává problém, když nějaká úloha trvá dlouho, a tím blokuje ostatní úlohy. Toto v důsledku blokuje celý prohlížeč a jeho hlavní vlákno tzv. main thread.
Dlouhé úlohy trvají déle než 50 ms a při měření v DevTools prohlížeče je poznáte podle červeného zvýraznění.
setTimeout() a optimalizace INP
Pro optimalizaci vysoké hodnoty metriky INP, je nutné rozdělit kód podle důležitosti, odložit méně důležité části a díky tomu nechat prohlížeči prostor, pro zpracování dalších uživatelských interakcí.
Před optimalizací uživatel po vstupu („Input received“) musí čekat na provedení dlouhé úlohy („Task“) na spuštění akce („Event“). Po optimalizace je dlouhá úloha rozbitá na menší a tudíž je akce spuštěna dříve.
V JavaScriptu se s dlouhými úlohami dá vypořádat využíváním tzv. asynchronních operací. Mezi ně patří API časovačů v čele s funkcí window.setTimeout()
. Ta umožňuje odložit námi vybrané úlohy. Funkce setTimeout()
má navíc jednu unikátní vlastnost. Pokud se nastaví čas nulový zpoždění, je dost pravděpodobné, že prohlížeč danou úlohu zpracuje odloží do následujícího renderovacím cyklu:
setTimeout(() => {
kod_odlozeny_do_nasledujiciho_renderu();
}, 0);
Díky této vlastnosti lze poměrně rychle kód rozdělit a méně důležité úlohy odložit o jeden renderovací cyklus. Tím vznikne tolik potřebný prostor, kdy si prohlížeč převezme kontrolu a vykreslí výstup důležitého kódu na obrazovku:
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);
}
V aktuálním renderovacím cyklu provedeme update v UI. Zapsání události do databáze a analytiky odložíme a provedeme asynchronně. Díky tomu rozdělíme úlohu na dvě.
Dobrý sluha ale zlý pán
Nic není zadarmo. Tato metoda optimalizace vypadá jednoduše, ale v jednoduchosti je často ďábel. Jsou situace, kdy vám odložení kódu pomocí setTimeout
může způsobit komplikace.
Pozor na analytiku
Odložení trackování informace o prokliku odkazu, může způsobit analytickou katastrofu. Pokud proklik vyvolá načítání nové stránky (tím není myšlen SPA routing), běh JavaScriptu se pozastaví a kód odložený pomocí setTimeout(fn, 0)
se nezavolá. Odkládejte jen to měření kde víte, že uživatel zůstává na stejné stránce.
Množství odložených úloh
Úlohy se pomocí setTimeout(fn, 0)
pouze řadí do fronty k pozdějšímu zpracování. Pokud odsunete hodně úloh a práce, vytvoříte si opět dlouhou úlohu v budoucnu. Navíc musíte myslet na to, že prohlížeč má na práci i jiné věci než zpracovat vaše časovače. Odkládejte vědomě a opatrně. Také si můžete trochu hrát s druhým parametrem API, tedy časem, a tím virtuálně ovlivňovat prioritou úloh.
JavaScriptové frameworky
Všechny moderní JavaScriptové frameworky mají vlastní mechanismus na zpracování úloh a interakcí. Neopatrným zavoláním setTimeout(fn, 0)
by mohlo dojít ke konfliktu stavů. Vždy se zajímejte jaký je ve vašem frameworku doporučený postup pro odložení kódu o jeden renderovací cyklus. A u Reactu si dejte pozor, useEffect
není vždy asynchronní.
Celé je to „hack“
Dá se říci, že z pohledu optimalizace dlouhých úloh je používání funkce setTimeout()
hack. Přece jen je to API, které slouží k načasování a spuštění úlohy někdy v budoucnu.
Spojení s výkonem prohlížeče zde není žádné a to, že se kód vykoná v následném cyklu renderování, je spíše vedlejší účinek. Specifikace na tuto potřebu reaguje a vznikají nová API jako scheduler.postTask()
a scheduler.yield()
. Globální podpora v prohlížečích zatím není. S využitím „graceful degradation“ lze samozřejmě s API experimentovat.
Rozdíl mezi setTimeout
a scheduler.yield()
. Úloha rozdělená pomocí yield bude prioritnější než další úlohy ve frontě.
Jiné alternativy k odložení úlohy
V javascriptu samozřejmě existuje více způsobů jak vykonání nějakého kódu odložit. Je to s nimi ale podobně jako se setTimeout, primárně slouží k jinému účelu.
- API
window.requestAnimationFrame()
je často zmiňovaná alternativa. Slouží ale k synchronizaci vypočtených stylů s DOM prvky. Tzn. je to ideální metoda pro animované prvky, které se animují pomocí javascriptu. Pokud byste toto API používali k “rozdělení úloh” pravděpodobně rychlost odezvy jen zhoršíte. - API
window.requestIdleCallback()
sice slouží k optimalizaci rychlosti a odložení práce do doby až má prohlížeč klid. Čas zpracování ale nemáte vůbec pod kontrolou a tak pro některé akce, jako je třeba analytika, může být úplně nevhodný. - API
MessageChannel()
, se také dá využít pro odložení kódu do následného cyklu renderování. Interně ho využívá React. Jde ale opět jen o další hack.
Závěr
I když víme, že setTimeout je „hack na optimalizaci INP“, má jednu zásadní výhodu. Jeho použití je velmi jednoduché a rychlé! Podporou jde zatím o jediné neprůstřelné řešení, protože API časovačů je v javascriptu odpradávna. Podívejte se na další metody optimalizace INP.