Optimalizace React aplikací

Blueberry
Blueberry_cz
Published in
6 min readDec 22, 2017

Jan Prášil a Jiří Orság

Po vytvoření aplikace v Reactu či v jiné JavaScript knihovně možná zjistíte, že má produkční build aplikace větší velikost, než jste předpokládali. Případně, že načítání stránky až do doby, kdy vám prohlížeč umožní s webovou stránkou manipulovat, trvá déle, než by mělo.

My jsme si tyto příklady vyzkoušeli na úkolu od jednoho z našich klientů, když jsme se měli podívat na performance React aplikací, které jsme vyvinuli před nějakou dobou a které běží v mnoha zemích světa. Přání klienta bylo zaměřit se zejména na země, kde je internetové připojení nějak limitováno, například země latinské Ameriky. Ovšem obecně se vzrůstajícím počtem mobilních zařízení je tento problém aktuální v podstatě globálně.

Mnoho programátorů by se v tento moment mohlo zděsit a hledat, co je špatného v kódu, protože “takhle velká aplikace přece není”. Naším cílem proto bylo podívat se na problém z druhého pohledu, tedy co můžeme udělat pro zrychlení načítání aplikace, aniž bychom do kódu byli nuceni velkou mírou zasahovat.

Prvotní hledání problému

Abychom zjistili, co se vlastně s naší aplikací děje, začali jsme s použitím jednoduchého nástroje od Pingdomu, který nám dal alespoň základní představu o tom, kde by se onen problém mohl ukrývat.

Dalo by se namítat, že tyto testovací nástroje závisí pouze na jediném pokusu a společně s latencí serveru, který testování provádí, poskytují zkreslené výsledky. Nicméně nám se díky tomuto nástroji podařilo analyzovat první problémy, kterými byl především vysoký počet požadavků a velikost celé stránky.

Dále jsme využili developerských nástrojů v Chromu. Analýzou všech požadavků, jejich velikostí a doby načtení celé stránky, jsme se dozvěděli, že jedním z hlavních problémů mohou být obrázky, které zabíraly velkou část celkově staženého obsahu. Zajímaly nás především hodnoty DOMContentLoaded. Po načtení DOMu totiž lze stránku běžně používat.

Velikost bundlu

Abychom našli moduly, které zbytečně zvětšovali velikost naší aplikace, rozhodli jsme se použít webpack plugin webpack-bundle-analyzer, jehož výsledkem byla dobře graficky znázorněná velikost jednotlivých souborů a balíčků v rámci všech bundlů.

Díky tomuto závěru jsme tak mohli najít moduly, které zbytečně zvětšovali velikost aplikace.

Moment.js

Moment.js je lokalizační knihovna, která obsahuje nejrůznější jazyky. Pro naše potřeby jednojazyčného projektu, ale jiné jazyky než námi vyžadovaný, nepotřebujeme.

Abychom odstranili závislosti na dalších jazycích, použili jsme plugin ContextReplacementPlugin následovně:

Tento plugin nalezne pomocí prvního parametru cestu, druhý parametr pak určuje, které soubory by měly zůstat.

Redukce nepoužitého kódu modulů

Dalším problémem se staly knihovny. Ty se vkládaly do projektu celé, ale používalo se z nich pouze několik málo, ne-li jen jedna, funkce. Pro řešení situace jsme měli na výběr tři možnosti:

  • Zkopírování vybraných funkcí do projektu a odstranění balíčku
  • Nalezení knihovny menší velikosti a nahrazení
  • Modulární import — importovat celou cestu k balíčku

Příklad modulárního importu

Bluebird

Bluebird je knihovna pro práci s Promise. Oproti nativní Promise knihovně je rychlejší a obsahuje spoustu rozšíření pro programátory.

Její nevýhodou je její velikost — zvláštně pokud programátor nevyužívá veškerou její funkcionalitu. Je ovšem možné si tuto knihovnu customizovat a to pomocí následující příkazu:

V možnosti features je pak možné uvést námi chtěné funkce.

Babel-polyfill

Další výrazně velkou knihovnou je babel-polyfill. Ten slouží pro emulování ES2015 a novějších prostředí. Některé prohlížeče však tuto funkcionalitu již podporují a není třeba mít knihovnu zahrnutou.

Pro zmenšení velikosti bundle jsme měli tři možnosti:

  • Podporovat pouze prohlížeče, které obsahují veškerou funkcionalitu
  • Vložit polyfill jako skript z CDN, např. Polyfill.io
  • Vkládat polyfill podmíněně a tím vytvořit více bundlů pro jednotlivé prohlížeče

V následujícím kódu pak můžete vidět, jakým způsobem toto podmíněné vkládání probíhá

Překlady a React Intl

Pokud používáme v aplikaci překlady, je při běhu aplikace jedna z nejpoužívanějších funkcí nalezení daného překladu podle identifikátoru, což v našem případě zajišťoval balíček react-intl.

Abychom zmenšili velikost, rozhodli jsme se při buildování aplikace nalézt všechny nepoužívané klíče a ty následně odstranit.

Pro zrychlení běhu aplikace jsme pak všechna volání funkce, která překlad zajišťuje, nahradili předpočítanými hodnotami.

Vendors

Abychom rozložili aplikaci do více částí a tím pomohli lépe cachovat a načítat aplikaci, rozhodli jsme se pro vytvoření vendor bundlu, které probíhá v konfiguračním souboru webpacku.

Pro vytvoření vendor skriptu si můžeme definovat, které balíčky budou v tomto bundle.

A následně je přidat pluginu CommonsChunkPlugin. V případě, že nechceme balíčky definovat výpisem, můžeme použít parametr minChunks v nastavení a definovat tak vkládané balíčky podle vlastní funkce. Na ukázkovém kódu pak vidíte, že do vendor bundle jsou vkládány pouze ty balíčky, které obsahují další závislosti.

Gzip middleware

Do webpacku jsme následně ještě přidali kompresi aplikace ihned po jejím buildu pomocí balíčku compression-webpack-plugin. Díky tomu jsme vytvořili komprimovanou verzi. Tento proces proto nemusel probíhat na straně serveru, což zvýšilo jeho reakční dobu.

Poté bylo na straně serveru potřeba přidat middleware, který při požadavku na JS soubor poslal komprimovaný soubor.

Další způsoby zrychlení aplikaci je možné provést na straně Express serveru.

Skripty třetích stran

Chceme-li aplikaci co nejrychleji načíst tak, aby s ní bylo možné co nejdříve manipulovat, je třeba odsunout načítání skriptů třetí stran až do doby, kdy je aplikace načtená.

Toho lze dosáhnout tak, jak je to popsáno v následujícím skriptu. Přidáním event listeneru na DOMContentLoaded a poté provedením vyrenderování skript tagů do kódu stránky, který v našem případě provádí funkce renderOnLoad.

Express cache

Jiným způsobem, jak zrychlit servírování stránek, je cachovat na serveru vyrenderovaný obsah pomocí balíčku lru-cache.

To funguje tak, že při prvním navštívení stránky se obsah uloží do cache a při dalších požadavcích na danou stránku se odpovídá již uloženým obsahem.

Samozřejmě lze tento způsob používat pouze u stránek, které jsou stejné pro všechny uživatele, a stránek, jejichž obsah se mění zřídka. Nejlépe lze tento přístup využít na domovské a dalších informativních stránkách. Na personalizovaných stránkách či stránkách, kde dochází k časté změně obsahu, by tento přístup jistě nefungoval.

V následujícím kódu můžete vidět způsob cachování obsahu.

WebP formát

WebP je obrázkový formát, který vytvořila společnost Google. V průměru dokáže uspořit 30 % velikosti oproti JPEG při stejné kompresi. Podporovány jsou všechny prohlížeče založené na Webkitu kromě Safari, což globálně odpovídá zhruba 74 % prohlížečů (dle dat caniuse.com).

Co se týče obrázků PNG s velkou alpha vrstvou, může být tento rozdíl ještě větší. V některých případech jsme dosáhli dokonce 90% úspory.

Kde mohou nastat problémy

Samozřejmě ne všechny dobré nápady vedly k úspěchu. Jedním z nich byla snaha zmenšit obrovskou knihovnu Lodash. Ta je však použitá jako závislost v dalších mnoha použitých knihovnách, která velikost Lodash nikterak neoptimalizovala a balíček nám tak zůstal celý v rámci našeho bundle.

Také jsme se pokusili použít prepack.io, který eliminuje výpočty, jež je možné předpočítat v rámci buildování aplikace. Prepack.io je však v současné době v development verzi a není doporučeno používat jej v produkční verzi aplikací.

V neposlední řadě jsme se pokusili použít bundle splitting dle jednotlivých stránek, resp. Route v React-Router. Mysleli, že tento postup bude velmi produktivní, nicméně po vygenerování jednotlivých bundlů jsme došli k zjištění, že hlavní bundle má stále kolem 95 % původní velikosti. Další části byli až zanedbatelně malé, protože na domovské stránce byly použity téměř veškeré komponenty, které se v aplikaci dále používají. Jelikož se velikosti hlavního bundle téměř nezměnila, rozhodli jsme v bundle splitting nepokračovat.

Výsledky

Na následujících obrázcích můžete vidět rozdíl před, resp. po, aplikování optimalizací v konzoli Google Chromu.

Znatelné je především doba načtení DOM — stránku je tak možné použít téměř o polovinu času dříve na tomto konkrétním projektu.

Podle statistik z Google Analytics je i zde vidět výrazné zrychlení a to v průměru o téměř 30 %.

--

--