čtvrtek 2. listopadu 2017

Jak zvýšit výkon React aplikací?


V době, kdy jsem objevil svět javascript knihoven a frameworků (React či Angular), jsem si říkal: "Sakra, to sice vypadá skvěle, ale musí to být příšerně pomalé, ne?"

Pravdou je, že ano i ne. Vše závisí na tom, jak k dané knihovně přistupujete a jak dobře znáte samotné principy, na kterých je daná knihovna postavena. V případě Reactu je samotný princip až stupidně jednoduchý, který z něj ovšem dělá onu krásu. Tady, stejně jako v životě, platí, že ty nejjednodušší varianty jsou ty nejsprávnější.

Pochopit princip, jak funguje React je vcelku jednoduchý. Je to silně komponentově orientovaná knihovna, která využívá principu one-way binding a immutable stavu. Co to ve výsledku znamená, je to, že na porovnání změny stavu Vám stačí dát mezi dva objekty tři rovnítka (===). Ano, to je vlastně celé. Tedy skoro... :)

Tento článek je určen pro všechny, kteří už prošli první zkouškou a tou je napsání "Hello World" v Reactu. Postupně si rozebereme jednotlivé části, které nám mohou ušetřit problémy s výkonem v React aplikacích.

1. Render metoda musí být rychlá

React se točí kolem jedné jediné věci a tou je render. Render fáze se Vám zavolá vždy, když dojde ke změně stavu. Ať už je to na úrovni props či state. Druhou variantou, kdy dochází k volání render fáze je v době, kdy je samotný předek znovu renderován.

Pokud si tyhle věci spojím, tak se dostanu k tomu, že render metoda se volá častěji, než se na první pohled zdá.

A zde je první poučka: "V render fázi nikdy nevykonávejte náročné operace."

A jak zjistit, kolikrát mi daná komponenta volá render? Na to stačí jednoduchý trik. Prostě a jednoduše si do metody dejte "console.log" a podívejte se sami. Šílené, že? :)

2. Nezapomínat na unikátní klíče u listů

Pokud v Reactu vytváříte tabulku či seznam, tak jste si jiště všimli, že když v dané komponentě, která je v iteraci, nemáte definován atribut key, tak Vám React bude hlásit warning. Ten warning neignorujte. Jde totiž o to, že díky tomu, že dané komponenty jsou ve virtuálním DOMu na stejné úrovni, pomáhá to Reactu s indexací. Výsledek je, že pakliže ony klíče nebudete definovat, nebo nebudou unikátní, tak výkon Vaší aplikace bude klesat.

Příklad s atributem key:
import * as React from 'react';

interface Props {
    readonly users: Array<{ id: string; firstName: string; lastName: string; }>;
}

const UsersTable: React.SFC<Props> = ({users}) => (
    <table>
        {users.map(({id, firstName, lastName}) => (
            <tr key={id}>
                <td>{firstName}</td>
                <td>{lastName}</td>
            </tr>
        ))}
    </table>
);

Tady je vidět, jak správně pracovat s iterovanými prvky v JSX.

3. Snažte se o ploché struktury

Na začátku jsme mluvili o tom, že render metoda se volá vždy, když je renderován rodič. Z toho nám vychází jedno důležité pravidlo a tím je, že bychom se měli snažit více o plochou strukturu. Ve výsledku to znamená, že čím máme hlubší struktury našich komponent, tím se zvyšuje riziko vykonnostních problémů.

Vím, že je to někdy těžké, ale netvořte v React aplikacích vlastní šílené univerzum. Atomizace jednotlivých částí je naprosto v pořádku. Nicméně, atomizace prvků neznamená větší hloubku.

4. Callbacky v render bez bind

Dalším pravidlem je nepoužívat bind(this) v callback funkcích v render.

Pojďme si udělat příklad:
import * as React from 'react';

interface Props {
    readonly onClick: (id: number) => void;
}

class Component extends React.Component<Props> {

    handleOnClick() {
        this.props.onClick(1);
    }

    render() {
        return (
            <button onClick={this.handleOnClick.bind(this)}>Klikni</button>
        );
    }

}

Tento kód trpí jedním neduhem. Tím je samotný bind. Pokud jsme si řekli, metoda render je volána často, tak tento kód nám způsobuje, že je stále dokola vytvářena nová funkce.

Pojďmě se podívat na správný přepis:
import * as React from 'react';

interface Props {
    readonly onClick: (id: number) => void;
}

class Component extends React.Component<Props> {

    handleOnClick = () => {
        this.props.onClick(1);
    };

    render() {
        return (
            <button onClick={this.handleOnClick}>Klikni</button>
        );
    }

}

V podstatě jsme funkci handleOnClick přepsali do arrow funkce, což nám umožňuje to, že samotné this, je this třídy a tím pádem ho nemusíme expresivně definovat přes bind(this).

5. Nevytvářejte zbytečně nové objekty

Pokud jsme se bavili o tom, že React funguje na základě třech rovnítek, které nám říkají, zda se jedná o stejný či jiný objekt, tak ve stejném duchu musíme přemýšlet i my.

Pojďme si udělat jednu ukázku a říci, co je na ní špatně:
import * as React from 'react';
import {Detail, Menu, Page} from './components';

class Container extends React.Component {

    getUser() {
        return {firstName: 'Ales', lastName: 'Dostal'};
    }

    render() {
        return (
            <Page user={this.getUser()}>
                <Menu/>
                <Detail/>
            </Page>
        );
    }

}

Na tomhle kódu je špatně jedna věc a tou je samotný fakt, že s každým voláním render metody se stále dokola vytváří objekt user.

Přepis by vypadal následovně:
import * as React from 'react';
import {Detail, Menu, Page} from './components';

const user = {firstName: 'Ales', lastName: 'Dostal'};

class Container extends React.Component {

    render() {
        return (
            <Page user={user}>
                <Menu/>
                <Detail/>
            </Page>
        );
    }

}

Tomuto způsobu se říká "memorizování", kdy pokud víme a máme objekt jasně definovaný, tak ho nechceme stále vytvářet dokola. Z podobným principem se můžeme setkat i jinde (viz níže).

6. S Reduxem používejte reselect

V případě, že používáte Redux, často se setkáváte s tím, že na jedné straně máte nějak uložená data v Redux store a jinak je potřebujete prezentovat do komponenty. První co se nabízí, že v metodě mapStateToProps budete své props různě přepočítávat. Je velká pravděpodobnost, že tímto způsobem budete s každou změnou v Redux store vytvářet i nové objekty ostatních atributů.

Jelikož by ukázka vydala na samotný článek, odkážu rovnou na knihovnu, kterou doporučuji použít: Reselect.

Tato knihovna vychází z principu memorizování dat, čímž pádem snižuje opakovanou tvorbu objektů v props a snižuje tím i nutnost volat častěji render fázi.

7. Občas použijte shouldComponentUpdate

Jsou situace, kdy se dostanete do fáze, že Vaše render fáze je drahá. To znamená, že již víte, že nelze moc najít způsoby, jak jí ještě více optimalizovat a snižovat náročnost aplikace.

Já jsem se s tímto setkal třeba u grafů Highcharts, které v React aplikacích občas používám.

V té chvíli se nabízí další možnost, jak zlepšit výkon React aplikace a tím je využití metody shouldComponentUpdate. Tato metoda má ve výchozím stavu nastaveno "return true", čímž říká, že render se volá tak, jak jsme si popsali. Pokud danou metodu přepíšete, máte možnost určit, za jakých podmínek se bude render metoda volat.

Pojďme si udělat malý příklad:

import * as React from 'react';
import {Options} from 'highcharts';
import * as ReactHighcharts from 'react-highcharts';

interface Props {
    readonly config: Options;
}

export class Highcharts extends React.Component<Props> {

    shouldComponentUpdate(nextProps: Readonly<Props>, nextState: Readonly<{}>, nextContext: any): boolean {
        return (nextProps.config !== this.props.config);
    }

    render() {
        return (
            <ReactHighcharts config={this.props.config}/>
        );
    }
}

Jak si můžete všimnout, tak jsem v dané ukázce použil kontrolu třech rovnítek, kde říkám, že pokud se props skutečně změnilo, spusť render. V opačném případě nikoli.

Pokud píšete React aplikaci, ale stále máte výkonnostní problémy, tento způsob je jeden z nejefektivnějších, jak zařídit, aby Vaše aplikace byla opět svižná.

Bohužel to sebou přináší i jedno riziko. Jak jste si jistě všimli, v nadpisu jsem použil slovo "občas" a to zcela záměrně. Jde totiž o to, že někoho by to mohlo vézt k tomu, že vlastně ve všech komponentách bude přepisovat shouldComponentUpdate. Jenže je tam jedno velké ALE. Tím ale je samotný fakt, že byste s každou změnou props a vůbec modelu museli neustále sofistikovaně upravovat implentaci této metody a věřte, že byste se dostali do fáze, kdy by Vaše aplikace nereagovala, tak jak si představujete. A jediným důvodem by byl lidský faktor.

Tím chci říct, že je naprosto korektní, že se u některých komponent volá render velice často. Pokud jsou dané komponenty optimalizovány a v render metodě nepočítáte model vesmíru, tak to nevadí :)

Další variantou jsou PureComponents, o kterých se můžete dočíst v manuálu.

8. Pomáhejte si dalšími nástroji

První věcí, kterou do svých projektů vždy přidávám je tslint a tslint-react. Jedná se o nástroj, který provádí statickou analýzu kódu, který píšete. Výhodou tslintu je i v tom, že má některá pravidla, která jsou nastavena za učelem upozorňování na možné výkonnostní problémy.

Pokud zůstaneme u ladění výkonu, tak další věcí, kterou občas používám je console.log. Ano, nestydím se za to. Naopak. Občas se chci rychle podívat, jak často se mi některá z metod renderuje a je to naprosto korektní způsob. A nemusíte se bát, tslint Vám vynadá za to, že ve svém kódu máte console.log. Proto ho používám pouze pro ladící účely.

K ladění výkonu je vhodný i Chrome DevTools - Performance. Díky tomu můžete provést sofistikovanou analýzu toho, jak se vaše komponenty chovají a kde vzniká potencionální výkonnostní problém. Více se můžete dozvědet z manuálu.

Závěr

Vypsat všechny známé tipy na zvýšení výkonu React aplikací, by bylo trochu mlácení prázdné slámy. Tento článek berte tak, že je dobré se hlavně zamýšlet nad tím, jak Vaše knihovna funguje a podle toho k ní také přistupovat. Pokud bychom k Reactu přistoupili naprosto nekriticky, tak bychom jednoduše mohli skončit s aplikací, která bude uživatelsky nepoužitelná.

Stále mějte na mysli to, že i když React má virtuál DOM, který nám částečně pomáhá se samotným výkonem, tak nás to úplně neochraňuje od toho, abychom neměli výkonnostní problémy.

Pokud Vás napadá další zajímavý tip na zvýšení výkonu, budu rád, když zanecháte komentář.

Žádné komentáře:

Okomentovat

Když programátor založí a řídí firmu

Jako malý jsem chtěl být popelářem. Ani ne tak proto, že bych měl nějaký zvláštní vztah k odpadkům, ale hrozně se mi líbilo, jak...