Zvyšte rychlost vašeho JS kódu

Pokud je pro vás rychlost kódu důležitá, neměl by vás tento článek minout. Je možné, že objevíte něco, co můžete ještě zlepšit.
Nálepky:
Rychlost kódu je důležitá. Čím rychlejší ten váš bude, tím víc budou vaši uživatelé spokojeni. Cílem tohoto článku není ukázat pokročilé techniky optimalizace v rozsáhlých aplikacích. Spíše předvedu nejrychlejší varianty jednoduchých operací, které se ve vašem kódu běžně vyskytují.
Důležité je však také říci, že ne vždy by měla mít rychlost přednost před čitelností a jasností. Některé zde ukázané techniky mají jednu nevýhodu – ne všem je hned jasné, co dělají a k čemu jsou. Proto před jejich použitím nejdříve zvažte, zda mají opravdu smysl.
A ještě jedna poznámka. Tento článek není o frázích typu “nepoužívejte konstrukci with”, “eval je zlo” nebo “vyhněte se try-catch-finally”. Místo toho vám řeknu, co používat, a ne co nepoužívat. Jdeme na to.
Porovnávání
JavaScript nabízí dvě možnosti porovnávání – trojité rovnítko (===
) a klasické dvojité (==
) a samozřejmě jejich ekvivalenty v nerovnosti. Určitě víte, jaký je mezi nimi rozdíl a určitě jste slyšeli, že se doporučuje používat to trojité.
V některých případech je použití dvojitého opodstatněné. Například když potřebujete, aby se "2"
rovnalo 2
. A to je právě ta největší slabina dvojitého rovnítka. Pokud jsou totiž operandy různého datového typu, tato operace provede nejdříve jejich konverzi. A to trvá.
Oproti tomu rovnítko trojité nic takového nedělá. Prostě jen porovná hodnoty. Žádné přetypovávání neprovádí. Obě varianty porovnání dosahují stejných rychlostních výsledků, když jsou operandy stejného datového typu. Pokud se však typy liší, nastane určité zhoršení výkonu při porovnávání pomocí ==
.
Nabízím samozřejmě důkaz. A proto, používejte ===
všude, kde není dvojité rovnítko nutností.
Přetypovávání na bool
Určitě jste se již setkali s podobným výrazem jako !variable
, a pravděpodobně i s !!variable
. Je to klasická operace negace, tudíž z každé pravdivé hodnoty (každé číslo kromě nuly, neprázdný řetězec, atd.) udělá false
, v opačném případě true
. Varianta s dvěma vykřičníky pouze překlápí znegovanou hodnotu na opačnou.
A hned se dostáváme k oné čitelnosti. Tomu, kdo se s JavaScriptem nesetkává každý den, může !!variable
na moment lehce zamotat hlavu. Obecně se k přetypovávání na bool doporučuje spíše Boolean(variable)
(pozor, ne new Boolean(variable)
!), kvůli tomu, že je hned jasné, co se zde děje. Avšak varianta s dvěma vykřičníky je znatelně rychlejší.
Dělení mocninami dvou
Jak často dělíte dvěma? Pokud mnohokrát, věděli jste, že stejného výsledku můžete docílit pomocí bitového posunu vpravo? A nejenom dvěma, ale všemi mocninami čísla dvě. Takže 16 >> 1
je to samé jako 16 / 2
, až na to, že první varianta je rychlejší. Stejně tak to platí pro 16 >> 2
(16 / 4
), 16 >> 3
(16 / 8
), a podobně.
Opět předvádím důkaz mého tvrzení. Zajímavostí je, že podobnou techniku lze využít také při násobení pomocí bitového posunu vlevo (3 << 1
je stejné jako 3 * 2
), ale z měření jsem zjistil, že zde není žádný výkonnostní rozdíl. JavaScript nejspíše při násobení provádí nějaké optimalizace. Takže můžete bitový posun používat jako machrovinku u obou operací, nicméně užitek to přinese pouze při dělení.
Převod na celá čísla
Pro převod řetězců nebo desetinných čísel na celá čísla nejspíše používáte funkci parseInt('123', 10)
. Je to tak vlastně správně (pozor však na určité záludnosti parseInt) a každý si je schopen odvodit, co daný kus kódu provádí. Nicméně existují i rychlejší varianty.
A zde se poprvé rozcházíme v prohlížečích. Zatímco v Chromu a Opeře jednoznačně válcuje konkurenci metoda Math.floor(value)
, ve Firefoxu dosahují nejvyššího výkonu bitové operace negace, součtu a posunu a skoro stejně je to v IE11, kde jen zaostává bitová negace. Aby toho nebylo málo, v Safari zas nejlépe vychází tradiční parseInt
. Sami se můžete podívat.
Takže se nám v podstatě nabízí dvě varianty – Math.floor(value)
nebo value | 0
. Podíl prohlížečů, kde je jedno nebo druhé rychlejší, je víceméně stejný, a tak nám ani toto hledisko nepomůže v rozhodování. Je to tedy čistě dle vašeho uvážení.
Konverze na řetězec
Stejně jako při převodu na čísla nebo boolean, i konverze na řetězec má svojí funkci, a sice String(value)
. Tento způsob však není téměř nikde nejrychlejší. Co se týče Chromu, tak tam jasně vede spojení s prázdným řetězcem pomocí operátoru plus ('' + value
). Dlouho to tak platilo i ve Firefoxu, ale v nových verzích kraluje metoda value.toString()
. Co se týče ostatních prohlížečů, vedení '' + value
převládá, proto asi mohu doporučit tento způsob. Zde je benchmark.
Dobrá zpráva je, že řešení s plusem funguje i s objekty. Co tím myslím je fakt, že daný objekt může mít vlastní implementaci metody toString()
, která může sloužit například k serializaci objektu. Naštěstí spojení s prázdným řetězcem vrátí ten samý výsledek, který by vrátilo přímé volání obj.toString()
. Nabízím jsfiddle, který to potvrzuje.
Procházení pole
Zapomeňte na funkcionální způsob arr.forEach(callback)
nebo starší for (var i in arr)
. Ty patří k tomu nejpomalejšímu, co můžete vůbec použít. Místo toho pole procházejte tradičním zápisem for smyčky, pouze si uložte velikost pole předem, ať při každé iteraci nepřistupujete k vlastnosti length
. Ovšem tohle pro vás určitě není žádná převratná novinka.
Zde se můžete podívat, jak forEach
a for...in
zaostávají za konkurencí. Klíčové slovo in
je ve všech svých použití pomalé, proto ho ani nepoužívejte k zjišťování přítomnosti vlastností v objektu ('prop' in obj
) a místo toho použijte klasické hranaté závorky (obj['prop'] !== undefined
) nebo tečkovou syntaxi (obj.prop !== undefined
). Přidám i důkaz.
Konkatenace polí
Určitě jste někdy potřebovali spojit dvě pole do jednoho. Po přečtení API k JavaScriptovým polím jste objevili metodu arr1.concat(arr2)
. Opět, každý ihned pochopí, k čemu je daný kus kódu určen. Nicméně existuje mnohem rychlejší způsob, jak spojit dvě pole, a sice pomocí konstrukce Array.prototype.push.apply(arr1, arr2)
.
Tento benchmark to potvrzuje. Pokud budete ve svém kódu tento způsob používat vícekrát, vyplatí se uložit si Array.prototype.push
do proměnné, ale je to spíše pro pohodlí, zrychlení není nijak závratné.
Procházení objektem
Jak jsme si již řekli, operátor in
není příliš šťastné používat. Jak tedy iterovat přes objekty, kde hodnoty nejsou uloženy na indexu, ale podle určitého klíče? Od ECMAScript 5.1 máme k dispozici Object.keys(obj)
, která vrací pole klíčů daného objektu. Bohužel tato metoda chybí ve starších verzích IE, konkrétně do verze 9. Pokud však nemáte v plánu tyto prohlížeče podporovat, nejrychlejší způsob, jak projít objekt, je tento:
for (var i = 0, keys = Object.keys(obj), len = keys.length; i < len; i++) {
obj[keys[i]];
}
Závěr
Pokud ve vašem krátkém skriptu změníte dvojité rovnítko na trojité, nečekejte žádné rapidní zrychlení. Avšak pokud již máte rozsáhlejší kód a aplikujete v něm všechny zmíněné optimalizace, zvýšení rychlosti již může být znát. A není to žádná hardcore teorie, pouze malé změny v běžných technikách. Zapamatovat si je není tak obtížné.
Jak jsem zmínil na začátku, optimalizace za každou cenu můžou velmi snížit čitelnost vašeho kódu. Také by se měly dělat až na hotovém kódu, ne při jeho psaní. Pokud znáte podobná vylepšení, určitě budu rád, když se s nimi nějakým způsobem podělíte, ať jsme zas o něco chytřejší.
Pokud potřebuji vyhodnotit „2“ porovnáno s 2 jako false, tak automaticky použiji ===. Pokud chci aby byl v tomto případe výsledek true, tak použiji ==. Navíc píšete, že pokud bude porovnání 2 == 2 bez rychlostního postihu, tak je to jedno.
Když pochází jeden operand zvenku (třeba jako vstupní parametr funkce), tudíž nemáte kontrolu nad datovým typem, a potřebujete, aby se striktně rovnal nějaké hodnotě, je škoda ztrácet čas typovými konverzemi.
No jo, ale pokud použiju nějaký framework jako Angular nebo Knockout a jiné, tak ty bottlenecky jsou někde úplně jinde a tyhle drobnosti už mě nevytrhnou.
Pak mám pocit, že spousta tipů může přestat platit, když vyjde nová verze prohlížeče. Takže mi vůbec nedává smysl nadřazovat v těchto případech výkon nad čitelnost.
Máte pravdu s těmi frameworky a v takovýchto aplikacích by se člověk měl zaměřit spíše na čitelnost a přehlednost, než aby aplikoval zmíněné techniky. Podle mě je jejich využití v nějakých menších javascriptových knihovnách, které pak budu třeba ve své aplikaci využívat. Dobrý příklad může být knihovny pro buildovací nástroje, které můžou pracovat nad velkým množstvím souborů a pro vývojáře je pak každá ušetřená milisekunda při buildu určitě potěšením.
A co se týče nových verzí prohlížečů, máte pravdu, ale v mnohých testech na jsPerf, kde bylo otestováno více verzí prohlížečů, zůstává většinou vítěz stejný (ale objevují se i výjimky).
Dobrý článek, škoda jen, že jsou ty techniky uvedné v pořadí od prakticky nejméně důležitých (dělení mocninami dvou se uplatní opravdu jen v extrémních případech) po velmi důležité (práce s poli).
K těm polím bych dodal, že všechny funkcionální metody (map, reduce, atd.) lze napsat mnohem rychleji prostým cyklem, a to nejen proto, že se ušetří volání funkce, ale taky proto, že ne vždy potřebujeme robustní implementaci dle ECMA normy, která zahrnuje různé type checkingy a hlavně ošetření sparse arrays. Další pomalou metodou je Array.prototype.indexOf – pokud pracujeme s dense poli, pak je mnohonásobně rychlejší ruční for cyklus. To se v nových verzích prohlížečů asi jen tak nezmění.
Podobné techniky jsem použil v jednom svém frameworku a opravdu to udělá dohromady klidně stovky procent výkonu. Jde o situace jako tisíce data-bindingů na stránce, kdy jsou podobné mikrooptimalizace znát. Takže pro knihovny a kritické části kódu rozhodně ano. Nevím, jak je na tom s optimalizacemi třeba právě Angular, pomalý je celkem dost. Knockout je naopak překvapivě docela svižný (aspoň co jsem testoval).
Ostatne to s temi prohlizeci plati vsude. Kdyz clovek pise v asm, tak to jiny procesor muze resit jinak, v C++ jiny kompiler atd. Takze vzdy pouzivat profiler a menit jen veci ktere jsou nezbytne nutne.
Uz jsem zazil pripady, kdy se jednou mikroopt. zkratil beh na 30%, u jineho programu na 18% casu, ale bez profileru bych na to neprisel. Take pak uz nebylo zrejme jak pokracovat, casy ve zbytku kodu byly ocekavatelne a nic nevycnivalo.
V článku chybí asi nejdůležitější informace — při jakékoliv optimalizaci je nejprve potřeba najít úzké hrdlo aplikace, kde dochází k největšímu zdržení — zrychlovat jiné místo nepřinese požadovaný efekt.
Nejprve je potřeba měřit pomocí profileru a pak teprve konat. Navíc v případě JS je potřeba měřit ve všech prohlížečích, protože různé JS enginy mají různé optimalizace.
Tento článek neměl být úplně o optimalizaci aplikace. Spíše jsem chtěl ukázat rychlejší varianty základních operací. Ale máte samozřejmě pravdu.
V 99.999 % zcela zbytečná optimalizace. Jak píše Kosek, nejdřív měř.
Samozřejmě správný postup je měření -> hypotéza -> kód -> goto 1. Tenhle článek nedává žádné informace o prvním kroku, ale může se hodit u toho druhého a třetího.
Za zmínku myslím stojí připomenout, že v případech, kdy se iteruje přes pole, které je ‚dense‘ (bez děr) a bez hodnot konvertcích na
false
, nabízí se pár postupů, které ani nejsou tolik znečitelňující, například:var i=arr.length; while(i--){arr[i]}
, což se vyplatí zejména v IE.var el, i=-1; while(el=els[++i]){el}
což je výkonově srovnatelné s ostatními navrhovanými obšírnostmi a pořád celkem neukecané.Podobná hustá pole bez nepravdivých hodnot jsou v běžné praxi myslím dost častá: všechna pole objektů což je mimo jiné i každý obyčejný
NodeList
vzniklý nějakýmquerySelectorem
nebodocument.getElementsByXY
je přesně tenhle případ (jsPerf s iterací přes pole HTML prvků).Mimochodem, do jsPerfu aktuálně odkazovaném v článku se vloudila drobná chybka do čtvrtého testu:
while (i++ < len) { arr[i]; }
‚přeskočí‘ první prvek a na konci ‚vrátí‘undefined
. (Mně se tohle taky stává každou chvíli :])Bitové operácie sú však len na 32 bitové čísla, teda sa môže stať, že to v dôsledku tej optimalizácie „pretečie“.
Overenie vlastnosti v objekte spôsobom obj[‚prop‘] !== undefined nerobí to isté ako ‚prop‘ in obj, lebo zahrňuje aj možnosť, že vlastnosť prop v obj existuje, no má hodnotu undefined.
Pokud pracuji s nějakým jazykem, měl bych se přeci naučit jeho syntaxi a vědět, jak funguje a jak se liší od ostatních. Nemůžu čekat, že jazyk pochopím na první pohled jen proto, že je C-like. A použití !! je prostě běžná praxe.
Tohle ale nebude fungovat u čísel přesahujících 32 bitů, neboť tento operátor pracuje s běžnými integery. Tím pádem ani nepočítá s desetinnou čárkou. Bylo by vhodné to zmínit.
Tam žádné záludnosti nejsou. Človek holt jen musí vědět, že když vynechá druhý parametr, bude ho funkce odvozovat podle vstupu.
Která však při vstupu „-25.5“ nesprávně vrátí -26. Co se týče ostatních metod, tak společný problém je, že číslo neparsují, ale pouze string přetypovávají. (Tedy např. ze stringu „25 Kč“ nic nedostanu.) U
str | 0
je stejný problém jako u bitového posunu, pracuje pouze s 32bitovým číslem. U stringu, kde vím, že je to prostě číslo, mohu jednoduše (a přehledně) použítstr * 1
.Mě by spíše fascinovala ta možnost, že by se to chovalo jinak. Při prvním přečtení mě to úplně zmátlo, osobně bych tohle z článku klidně odstranil nebo to napsal maličko jinak.
Ale s tímhle zase pomalu. Funkcionální způsob bych rozhodně neodsuzoval, neboť může v mnoha případech velice zkrátit a zpřehlednit kód. Na místech, kde se neprovádí složité výpočty, to uvítám spíše než for konstrukci.
Co je, prosím pěkně, toto za blábol? Cyklus for-in nikdy nebyl určen k procházení indexovaných kolekcí.
Vřele doporučuji vyhledat si rozdíl mezi použitím in operátoru a porovnáním s hodnotou undefined.
Opět bláboly. Metoda concat opravdu, podobně jako String.concat u stringů, slouží ke spojování polí. A je to jediná metoda spojování dvou polí do jednoho nového. Jedná se tedy o něco absolutně jiného než použití metody push, která pouze rozšíří první pole. Je to zvláštní, že vytváření nového objektu zabere více času, že? :-)
Ano, to jsem přesně říkal. Pokud jde o parsování, je toto jediná možnost.
Asi se jednalo o nějaký univerzálnější test, to bych neřešil.
Jak jsem psal výše, to člověk musí znát, pokud chce používat JS. Stačí se podívat do dokumentace. :-) Druhý parametr určuje číselnou soustavu. Např. výraz
parseInt("f", 16)
tedy vrátí číslo 15.Myslel som druhý argument funkcie parseFloat – ten je tam na čo?
Aha. Tak ta nemá žádný druhý argument. Takže to na funkci nemá vliv. ;-)