JavaScript timers - naše staré hodiny, bijí čtyři hodiny

Absolutním Cimrmanovým rýmem začínám další ze série článků o Javascriptu. V něm bych chtěl rozebrat pár postřehů při práci s časovači (timery) v JavaScriptu. Ty se používají k lecčemu - při jQuery animacích, zobrazování aktuálního času, periodickém dotazováním serveru atp. Intuitivně jsme vždycky tušili, že jejich časování nemusí být úplně přesné, ale přesto jsme hrubě podcenili význam pro aplikaci, pro kterou je aktuální čas zásadní.

Stáli jsme před relativně jednoduchým problémem. Odpočítávat čas do okamžiku T a vypočítávat slevu v ceně na základě času, který do okamžiku T zbývá. Samozřejmě všechny údaje (ať čas nebo cena) musely být u všech klientů naprosto stejné a musely se měnit každou vteřinu. Tento jednoduchý problém nás ale docela potrápil a proto vznikl tento článek, který by měl zachytit problémy a jejich řešení.

Všechny problémy byly způsobeny dvěma příčinami. Ta první je, že každý počítač klienta má odlišný čas a přes všechny synchronizace, které operační systémy provádí.. Nezanedbatelná část klientů má čas posunutý zásadním způsobem v řádech minut nebo i hodin (a to přesto, že se pohybujeme ve stejném časovém pásmu).

Druhou je nepřesnost časovačů v prohlížečích. Pro ilustraci jsem připravil jednoduchou testovací stránku, kterou si sami můžete spustit v různých prohlížečích. Především v těch, kde je horší implementace JavaScriptu (starší verze prohlížečů nebo Internet Explorer), můžeme během první minuty lehce nasbírat zpoždění i několika vteřin.

Timer tester

Vysvětlení důvodu, proč se tak děje se můžete dočíst v článku Johna Resiga, kde rozebírá princip toho jak časovače v prohlížečích fungují. Především je to dáno tím, že veškeré zpracování probíhá v jednom vlákně, takže v případě, že v danou chvíli prohlížeč vyřizuje jiný kus kódu (např. kód ošetřující kliknutí myši), spuštění časovače musí počkat (respektive se zafrontuje).

Z našich chyb jsme si vzali několik poučení

browser má vždy jiný čas než server

Nikdy nepracujte přímo s new Date() při vypočítávání zbývajícího času. Jediný způsob, jak synchronizovat čas všech klientů, je dostat na klienta aktuální serverový čas (i tak nebudete úplně přesní, protože mezi vložením času do HTTP response a zpracování HTTP response na klientovi nějaký čas - byť minimální - uběhne). Na klientu pak vypočtete odchylku systémového času klienta a systémového času serveru a o tuto odchylku upravujte všechna volání new Date() v logice klienta (nebo ještě lépe toto chování extrahujte do nějaké metody).

Příklad:


var serverTimeDifference;
function setServerTime(serverTime) {
   serverTimeDifference = new Date().getTime() - serverTime.getTime();
}
function getActualTime() {
   var actualTime = new Date();
   actualTime.setTime(actualTime.getTime() - serverTimeDifference);
   return actualTime;
}

na periodu časovačů se při výpočtech nelze spolehnout

V logice na klientu nikdy nepočítejte s intervalem, který řídí spouštění aktualizační logiky, ale s aktuálním systémovým časem upraveným o odchylku. Přestože je to lákavé, odpočítávání času by nemělo fungovat tak, že si nastavíte timer na 1 sekundový interval a při zavolání metody z proměnné obsahující počet zbývajících sekund odečtete jedničku. Takto odpočet nebude vůbec přesný.

Řešením je přepsat aktualizační metodu tak, že vždy nanovo vypočte počet zbývajících sekund odečtením cílového času udaného serverem od aktuálního času ošetřeného o odchylku.

Závěrem

Když si ten článek po sobě čtu, tak si říkám: "Otec Fura objevil Ameriku". Je zvláštní jak hodně dokáže být člověk i po těch letech naivní.

Reference