Lekce 9 · Výkon
Výkon a renderování
Kdy se komponenta překresluje, memo, useMemo, useCallback, useTransition, useDeferredValue.
Načítám lekci…
Lekce 9 · Výkon
Kdy se komponenta překresluje, memo, useMemo, useCallback, useTransition, useDeferredValue.
Načítám lekci…
Vrátíme se do kuchyně. Šéfkuchař (rodičovská komponenta) má pod sebou pomocníky (dětské komponenty). Kdykoli šéf změní svůj recept – třeba přidá sůl –, všichni pomocníci znovu projdou svůj recept a zkontrolují, jestli jejich talíř pořád odpovídá. Většinou zjistí, že ano, a nic nemění. Takovému projití receptu se říká render.
Projít recept je rychlé. Drahé je teprve skutečně něco vyměnit na stole – a to React dělá jen tam, kde se výsledek opravdu liší. Proto je většina renderů levná a není třeba je řešit.
Problém nastane, když má některý pomocník hodně práce – třeba přepočítává tabulku s tisíci řádky – a ostatní na něj musí čekat. Pak je aplikace pomalá: píšeš do pole a písmena naskakují se zpožděním. Tahle lekce je o tom, jak takové čekání najít a odstranit.
memo); odložení méně důležité práce na později (useTransition).Komponenta se renderuje (React zavolá její funkci), když:
Bod 2 překvapí nejvíc. Příklad:
function Dashboard() {const [note, setNote] = useState('')return (<><input value={note} onChange={(e) => setNote(e.target.value)} /><ExpensiveChart year={2025} /></>)}
note v Dashboard.Dashboard(). Ta vrátí JSX s <ExpensiveChart year={2025} />.ExpensiveChart() – i když year je pořád 2025. React props sám od sebe neporovnává. Šéf změnil recept, pomocník musí projít ten svůj.Dvě techniky, které zrychlí aplikaci bez jediného memo. Obě vycházejí z toho, že se překresluje komponenta se změněným stavem a všechno pod ní – nic nad ní ani vedle ní.
V příkladu výše potřebuje note jen input. Graf o něm neví. Tak proč je stav v komponentě, která obsahuje i graf? Přesuneme ho do samostatné komponenty:
function Dashboard() {return (<><NoteInput /> {/* stav note je uvnitř */}<ExpensiveChart year={2025} /></>)}function NoteInput() {const [note, setNote] = useState('')return <input value={note} onChange={(e) => setNote(e.target.value)} />}
Teď změna note překreslí jen NoteInput. Dashboard ani graf se nedozví nic – sůl se změnila jen v jedné omáčce.
Co když stav musí být v obalující komponentě – třeba barva pozadí, kterou používá obal kolem grafu? Pak pomůže trik s children:
// ❌ ColorPicker vytváří graf sám → při změně barvy ho překreslífunction ColorPicker() {const [color, setColor] = useState('white')return (<div style={{ background: color }}><input type="color" onChange={(e) => setColor(e.target.value)} /><ExpensiveChart year={2025} /></div>)}// ✅ Graf vytvoří rodič a předá ho jako childrenfunction ColorPicker({ children }: { children: ReactNode }) {const [color, setColor] = useState('white')return (<div style={{ background: color }}><input type="color" onChange={(e) => setColor(e.target.value)} />{children}</div>)}<ColorPicker><ExpensiveChart year={2025} /></ColorPicker>
Proč to funguje? Prvek <ExpensiveChart /> teď vznikl v rodiči ColorPickeru. Když se změní barva, React překreslí ColorPicker, ale children je pořád ten samý prvek z minula – nikdo ho znovu nevytvořil. React pozná, že se nezměnil, a graf přeskočí. Hotové jídlo přinesené z jiné kuchyně se nepřevařuje.
Když uspořádání nestačí, přijde na řadu pomocník, který se před prací podívá na objednávku: „Je stejná jako minule? Pak nic nedělám a podám minulý talíř.“ To je memo.
const ExpensiveChart = memo(function ExpensiveChart({ year }: { year: number }) {// …drahý výpočet…})
Má to ale háček. Pomocník neporovnává objednávky po obsahu, ale podle toho, jestli je to tentýž lístek (Object.is pro každý prop zvlášť). U čísel a textů je to totéž. Ale objekty a funkce vznikají při každém renderu rodiče znovu – je to nový lístek, i když je na něm napsané totéž:
function Dashboard() {const [note, setNote] = useState('')// Při každém renderu NOVÝ objekt a NOVÁ funkceconst options = { year: 2025, color: 'blue' }const handleClick = (point: Point) => console.log(point)return <ExpensiveChart options={options} onPointClick={handleClick} />// memo porovná: options === minulé options? NE → překreslí se stejně}
Řešením je ty hodnoty zapamatovat, aby rodič při dalším renderu podal tentýž lístek:
const options = useMemo(() => ({ year: 2025, color: 'blue' }), []) // stejný objektconst handleClick = useCallback((point: Point) => console.log(point), []) // stejná funkce
| Nástroj | Co dělá | Kdy použít |
|---|---|---|
memo(Comp) | Přeskočí render, když jsou všechny props stejné (podle reference). | Drahá komponenta, která se často překresluje se stejnými props. |
useMemo(fn, deps) | Zapamatuje VÝSLEDEK výpočtu. | Drahý výpočet; nebo stabilní objekt/pole pro memo komponentu či závislost efektu. |
useCallback(fn, deps) | Zapamatuje FUNKCI (= useMemo(() => fn)). | Funkce předávaná memo komponentě nebo použitá v závislostech efektu. |
Co vidíš: Rodič s počítadlem a čtyři dětské karty. Karta žlutě blikne, když se renderuje. Karty se liší v tom, jestli jsou obalené v memo a jaké props dostávají.
Vyzkoušej:
memo. Dostává objekt style a funkci vytvořené přímo v renderu – pokaždé nový lístek.useMemo a useCallback. Nebliká.Co z toho plyne: memo pomůže jen tehdy, když jsou props opravdu stejné reference. Jediný objekt nebo funkce vytvořená v renderu ho vyřadí.
Načítám interaktivní část…
V restauraci má přednost host, který právě vešel – musíš ho pozdravit hned. Leštění příborů může počkat, a když při něm vejde další host, leštění přerušíš a vrátíš se k němu později.
React umí totéž. Některé aktualizace jsou urgentní – psaní do pole, kliknutí; uživatel čeká okamžitou odezvu. Jiné jsou neurgentní – překreslení velkého seznamu výsledků. React umí neurgentní render začít, a když přijde urgentní, přerušit ho a vrátit se k němu později. Musíš mu jen říct, co je co.
function Search() {const [query, setQuery] = useState('')const deferredQuery = useDeferredValue(query) // „opožděná kopie“return (<><input value={query} onChange={(e) => setQuery(e.target.value)} /><SlowList query={deferredQuery} /></>)}const SlowList = memo(function SlowList({ query }: { query: string }) { … })
Co se stane, když napíšeš písmeno „a“:
query je „a“, ale deferredQuery je pořád stará hodnota „“. Input ukáže „a“ hned. SlowList dostane stejný prop jako minule a díky memo se přeskočí.deferredQuery je „a“ – teď se SlowList počítá.Co vidíš: Pole a pod ním seznam 400 uměle zpomalených položek (každý render seznamu trvá asi 120 ms). Zaškrtávátko zapíná useDeferredValue. Když seznam ukazuje starou hodnotu, zprůhlední.
Vyzkoušej:
Co z toho plyne: Pomalý seznam pořád trvá stejně dlouho. Ale už neblokuje psaní – React ho počítá „mezi písmeny“ a přerušuje ho.
Načítám interaktivní část…
Když máš přístup k setteru, můžeš neurgentní změnu označit přímo. Typicky přepínání záložek: kliknutí má odezvu hned, ale vykreslení obsahu pomalé záložky může počkat – a když mezitím klikneš na jinou záložku, React rozpracovanou práci zahodí.
const [tab, setTab] = useState('about')const [isPending, startTransition] = useTransition()function selectTab(next: Tab) {startTransition(() => setTab(next)) // tahle změna je neurgentní}{isPending && <Spinner />} // zatímco se nová záložka počítá
Co vidíš: Tři záložky. „Příspěvky“ jsou uměle pomalé (render trvá asi 300 ms). Zaškrtávátko zapíná useTransition.
Vyzkoušej:
Co z toho plyne: Transition dělá z pomalého přepnutí přerušitelnou práci na pozadí. Uživatel může klikat dál a vidí, že se něco děje.
Načítám interaktivní část…
| useTransition | useDeferredValue | |
|---|---|---|
| Co obalíš | Aktualizaci stavu (setX) | Hodnotu |
| Kdy | Máš přístup k setteru (přepínání záložek, navigace) | Hodnota přichází zvenku (props), nebo chceš mít pole okamžitě a seznam později |
| Jak poznáš čekání | isPending | value !== deferredValue |
lazy(() => import('./Page')) + Suspense. Prohlížeč stáhne kód stránky, až když ho potřebuje. Tenhle kurz načítá každou lekci zvlášť.// Líné načtení těžké komponenty (graf, editor, mapa)const Chart = lazy(() => import('./Chart'))<Suspense fallback={<Spinner />}>{showChart && <Chart data={data} />}</Suspense>
Zkus psát do poznámky a sleduj, jak cena bliká. Jedním obalením to zastavíš.
VÝZVA: Ať zbytečně nebliká
Price bliká při každém překreslení. Teď bliká, i když píšeš do poznámky,
která s cenou vůbec nesouvisí.
1. Obal komponentu Price do memo: const Price = memo(function Price(…) { … })
(memo importuj z 'react')
Pak bude blikat jen při změně ceny.Načítám interaktivní část…
První výzva je přesně příklad z kapitol 1–3: oprav ji nejdřív uspořádáním kuchyně, pak memoizací, a porovnej obě řešení. Druhá procvičí kapitolu 4 – pole musí reagovat okamžitě, i když se seznam počítá déle.
Najdi, proč je psaní pomalé, a oprav to – nejdřív strukturou, pak memoizací.
Střední a těžké výzvy jsou bonus pro přihlášené. Registrace je zdarma – stačí e-mail, žádné heslo.
Host u dveří (psaní) má přednost před leštěním příborů (seznam výsledků).
Střední a těžké výzvy jsou bonus pro přihlášené. Registrace je zdarma – stačí e-mail, žádné heslo.
children.useMemo/useCallback.useRenderFlash)useRenderFlash – ze souboru src/course/useRenderFlash.ts. Pomocný hook jen pro výuku: element blikne při každém renderu, ať je vidět, co se překresluje. Ve vlastním kódu ho nepotřebuješ./*** A teaching helper hook: the element briefly "flashes" after every render of the component.* An effect without a dependency array runs after every commit – exactly what we want to visualize.*/export function useRenderFlash<T extends HTMLElement>() {const ref = useRef<T>(null)useEffect(() => {const el = ref.currentif (!el) returnel.classList.remove('flash')void el.offsetWidth // restart the CSS animationel.classList.add('flash')})return ref}
row, stack, card, list, btn, input nebo muted jsou hotové styly kurzu v src/styles.css (row = prvky vedle sebe, stack = pod sebou, card = rámeček, muted = šedý text).