Article by Martin Topolánek
originally on medium.com
Jaký existuje rozdíl mezi dynamickým a statickým CSS in JS. Na co si dát pozor při použití styled-components a jak několikanásobně zrychlit načítání vašich webových stránek.
Co přináší éra SPA?
Mohli bychom říct, že rozdílů mezi MPA (multi-page application) a SPA (single-page application) je spousta. Najdete mnoho článků věnujících se tomuto tématu. V tomto svém příspěvku se chci zaměřit na dynamické přidávání stylů do CSSOM za běhu aplikace (run-time)
Pokud se neuchýlíte ke stylování za pomocí vytváření CSS tříd v čistém CSS nebo nesáhnete po preprocesoru typu LESS nebo SASS, pak použijete nějakou formu CSS in JS. V závislosti na knihovně, kterou použijete se CSS třídy přidávají buďto při sestavování projektu (build-time) nebo za běhu aplikace.
Firefox, výrazně rychlejší než Chrome
Ve firmě jsme dosud používali pouze knihovnu styled-components, která sestavuje výsledné CSS třídy za běhu aplikace. Nepřemýšleli jsme moc nad způsobem generování výsledných stylů do doby, než musel prohlížeč vykreslit stránku se zhruba 153 tisíci nody.
Rozdíly v rychlostí renderování mezi prohlížeči Chrome a Firefox byly extrémní. Níže přikládám výsledky profilování načtení stránky ve Firefoxu a Chrome.
Jen podotknu, že celkový rozdíl mezi načtením stránky ve Firefoxu a v Chrome byl cca 30 sekund.
První úspěch
Abychom zoptimalizovali aplikaci bylo zapotřebí zajistit, ať se nespouští nejdražší části vykreslovacího procesu — přepočet stylů a následný výpočet layoutu. Detailní článek na toto téma si můžete přečíst třeba zde.
Chrome zobrazuje u úkolů pro přepočet stylů a výpočtu layoutu i iniciátora. Je tedy možné se dopátrat ke zdroji a zjistit, co daný úkol spustilo. Ne vždy však dostanete smysluplnou odpověď. Je možné, že iniciátorem je skript knihovny, na které stavíte nebo jiná část vykreslovacího procesu (například plánované přepočtení stylů, apod.). Může se také stát, že iniciátorem úlohy je část vámi psaného skriptu.
Poslední výše zmiňovaná možnost se stala i nám. Zjistili jsme, že po načtení stránky spouštíme skript pro změnu velikosti písma pro kontejner ve kterém bylo obalených 150 tisíc nodů. Úprava této funkce nám ušetřila 4 sekundy z výsledného času.
Dobrý začátek ale co s těmi zbylými 24 sekundami? Pokud tedy beru v potaz naměřený čas za rendering (viz Obrázek 2 — kruhový diagram), což je část, kterou můžeme, jako frontendisti, do značené míry ovlivnit.
Detektivní práce
Načítání stránky nebyl náš jediný problém. Aplikace měla nízkou odezvu i při přidávání dalších komponent do DOMu.
Krásná ukázka může být otevření seznamu hodnot selectu na výběr velikosti fontu. Po kliknutí na tlačítko se na pozadí přidá do DOMu list (na pozadí se jedná o ul element). Spolu s touto akcí se přidají CSS třídy určené pro tuto komponentu.
Níže můžete vidět ukázku této komponenty včetně jejího profilování.
Z obrázku výše můžeme vyčíst, že rozbalení selectu trvalo cca 8 sekund. Avšak na druhý pokus se select rozbalil okamžitě. Výsledky profilování potvrzují teorii o přidávání CSS tříd do CSSOMu. Iniciátorem úlohy výpočtu layoutu (Layout) byl skript, který přidal na stránku CSS třídy pro zobrazení select nabídky. V momentě, kdy se přidaly CSS třídy, Chrome spustil přepočet stylů a výpočet layotu.
Plynulý přechod od styled-component
Na základě článků How to increase CSS-in-JS performance by 175x a The unseen performance costs of modern CSS-in-JS libraries in React apps jsme dospěli k názoru, že knihovna styled-components není pro nás to pravé ořechové. Hledali jsme tedy náhradu. Nejlépe knihovnu, která dokáže generovat styly za build-time. Nabízely se tyto knihovny:
GitHub – callstack/linaria: Zero-runtime CSS in JS library
Zero-runtime CSS in JS library. Contribute to callstack/linaria development by creating an account on GitHub.
GitHub – atlassian-labs/compiled: A familiar and performant compile time CSS-in-JS library for…
A familiar and performant compile time CSS-in-JS library for React. Get started now ➚ Turn on extraction and all…
Vybrali jsme si linarii a to na základě této analýzy. Opírali jsme se o fakta, že knihovna existuje na trhu již 5 let, je stále aktivní, implementují se nové úpravy a vývojáři reagují relativně rychle na vzniklé issues. Compiled na druhé straně je dle dostupných údajů na trhu pouhý 1 rok. Chtěli jsme mít jistotu, že knihovna, kterou nasadíme do produkce, je již osvědčená a proto jsme sáhli po Linarii, která je na trhu dostupná delší dobu.
Přechod ale nebyl okamžitý. Přejít z jedné knihovny na druhou nám přišlo příliš komplikované a časově náročné. Proto jsme zkusili nejdřív následující kroky, které bychom museli při přechodu na linarii i tak podstoupit:
- přepis JS proměnných (například proměnné definující design aplikace, tzv. theme) do CSS proměnných
- přepis prop atributů do data-atributů
Tyto kroky pomohly k optimalizaci v tom ohledu, že komponenta, která již byla přítomna v DOMu, byla rychlá při změně stavu. Přepis prop atributů na data-atributy způsobil, že se nemusely přidávat nové CSS třídy ale použily se stávající.
Závěrečné zhodnocení
Ve finále stavíme na linarii tři měsíce a strategie tvoření CSS tříd při buildu aplikace se nám vyplatila. Bohužel i přesto musím konstatovat, že knihovna má své mouchy a při vyvíjení aplikace lze poznat rozdíl oproti používání styled-components. Při používání se potýkáme s těmito problémy:
- při vyvíjení aplikace každá uložená změna způsobí, že se stránka obnoví 3x (souvisí s run-time kompilací stylů)
- za neznámých okolností se vyhazují zvláštní chyby (např. občas není možné použití forwardRef funkce, když ji linaria rozšiřuje)
- narozdíl od styled-components nemůžeme psát css funkci, ve které referujeme na jiné styled komponenty
Výhoda používání knihovny však převažuje nad jejími nevýhodami. Jsme otevření jiným možnostem, ale aktuálně je nevyhledáváme.
Níže přikládám výsledky profilování stejné stránky po přechodu na novou knihovnu.
Z profilování lze vyčíst, že rendering nyní trval 8,5 sekund, čímž je aplikace přibližně 3,5 násobně rychlejší oproti původním 27 sekundám.