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.
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:
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
- Článek Johna Resiga osvětlující prinicipy fungování Timerů v JavaScriptu
- Článek narážející na podobný problém a řešení odchylky klientského času od času serveru
- Zajímavé zjištění Johna Resiga o problémech spouštění javascriptových performance testů na platformě MS Windows – trošku off-topic ale velmi zajímavé čtení




Ten zaver do kamene tesat. Takhle naivni jsem i pres svuj seniorsky vek docela casto. Sam pred sebou se omlouvam slovy “Po bitve je kazdej general”.
Diky za clanek.
Nezouseli jste zmerit, jestli doba “mezi vložením času do HTTP response a zpracování HTTP response” odpovida zhruba polovine casu od odeslani requestu z klienta po zpracovani response? Pak by se to mozna mohlo ješte upravovat o tuto konstantu.
Abych pravdu řekl, nezkoušeli. Současná přesnost je pro nás dostačující – rozdíly mezi uživateli nejsou už rozpoznatelné. Ale je to zajímavý nápad.
O zobrazení serverového času u klienta jsem před časem také psal: http://php.vrana.cz/zobrazeni-serveroveho-casu.php
Doplním jen, že pokud bychom chtěli vyřešit nepřesnost časovače, dalo by se místo setInterval zavolat vždy setTimeout s vypočteným časem (mírně nižším než 1 s).
Podle mého názoru setTimeout trpí úplně stejnými nešvary jako setTimeout – princip je úplně stejný – jen setInterval se provádí opakovaně a setTimeout jedinkrát. Na vině je jednovláknovost prohlížeče, se kterou voláním jiné metody nic nezískáme. Každopádně díky za reakci.
Měl jsem na mysli http://php.vrana.cz/pravidelne-spousteni-javascript-kodu.php
Pochopil jsem co jste měl na mysli. Spíš na to reaguji ve smyslu – přestože v daném okamžiku víte, že máte zpoždění 50ms a načasujete tedy další timer na 950ms místo 1000s nikde nemáte zaručeno, že se vám timer skutečně zavolá za 950ms a ne třeba až za 1300ms. Podle mého názoru tím daný problém neřešíte.
Řeší to problém narůstajícího zpoždění.
Narůstajícího zpoždění možná, nikoliv však přesné zobrazení času v konkrétním okamžiku. Navíc se mi zdá, že třeba Firefox podobnou logiku uplatňuje již na setInterval – když na testovacím skriptu sleduji zpoždění, tak pokud naroste, tak se po nějaké chvíli zase sníží. Ale možná je to jen klam.
Koukal jsem na ten kód, co uvádíte, a nepovedlo se mi vyextrahovat, jak by se choval v případě, že by zpoždění přesáhlo celý sledovaný interval. Tzn. kdyby v okamžiku N bylo zpoždění 0ms a v okamžiku N+1 třebas +1300ms, přičemž námi sledovaná perioda je 1000ms. Pak byste se totiž dostal do situace, kdy byste potřeboval další timeout nastavit na -300ms, což nepůjde.
Na konci článku to zmiňuji. Řeším to žertem, v praktické implementaci by to asi chtělo funkci zavolat ihned znovu.