Přejít k navigační liště

Zdroják » JavaScript » Zvyšte rychlost vašeho JS kódu

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.

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ší.

Komentáře

Subscribe
Upozornit na
guest
17 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
Naith

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.

honzamarek88

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.

karfcz

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).

KarelI

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.

Jirka Kosek

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.

Daniel Steigerwald

V 99.999 % zcela zbytečná optimalizace. Jak píše Kosek, nejdřív měř.

Ladislav Thon

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.

Michal Čaplygin

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:

  • Procházet pozpátku (pokud to zrovna nevadí): var i=arr.length; while(i--){arr[i]}, což se vyplatí zejména v IE.
  • Procházet s kontrolou pravdivosti přiřazené proměnné 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ým querySelectorem nebo document.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 :])

e

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.

Radek_CZ

Tomu, kdo se s JavaScriptem nesetkává každý den, může !!variable na moment lehce zamotat hlavu.

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.

Takže 16 >> 1 je to samé jako 16 / 2

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.

pozor však na určité záludnosti parseInt

Tam žádné záludnosti nejsou. Človek holt jen musí vědět, že když vynechá druhý parametr, bude ho funkce odvozovat podle vstupu.

metoda Math.floor(value)

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žít str * 1.

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().

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.

Zapomeňte na funkcionální způsob arr.forEach(callback)

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.

nebo starší for (var i in arr)

Co je, prosím pěkně, toto za blábol? Cyklus for-in nikdy nebyl určen k procházení indexovaných kolekcí.

proto ho ani nepoužívejte k zjišťování přítomnosti vlastností v objektu (‚prop‘ in obj)

Vřele doporučuji vyhledat si rozdíl mezi použitím in operátoru a porovnáním s hodnotou undefined.

a sice pomocí konstrukce Array.prototype.push.apply(arr1, arr2)

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? :-)

e

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.)
Metódy parseInt a parseFloat však z reťazca „25 Kč“ vydolujú číslo 25.
Ak je však v článku spomínaný „převod řetězců nebo desetinných čísel na celá čísla“,
prečo sú v teste výrazy, čo to nerobia, ako +premenná, premenná*1 či metóda parseFloat?
A prečo má druhý argument 10?
Kde autor k tomu druhému argumentu prišiel?

Radek_CZ

Metódy parseInt a parseFloat však z reťazca „25 Kč“ vydolujú číslo 25.

Ano, to jsem přesně říkal. Pokud jde o parsování, je toto jediná možnost.

prečo sú v teste výrazy, čo to nerobia, ako +premenná, premenná*1 či metóda parseFloat?

Asi se jednalo o nějaký univerzálnější test, to bych neřešil.

A prečo má druhý argument 10?

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.

e

Myslel som druhý argument funkcie parseFloat – ten je tam na čo?

Radek_CZ

Aha. Tak ta nemá žádný druhý argument. Takže to na funkci nemá vliv. ;-)

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.

Pocta C64

Za prvopočátek své programátorské kariéry vděčím počítači Commodore 64. Tehdy jsem genialitu návrhu nemohl docenit. Dnes dokážu lehce nahlédnout pod pokličku. Chtěl bych se o to s vámi podělit a vzdát mu hold.