React Profiler & DevTools

React DevTools Profiler zobrazuje, které komponenty se re-renderují, jak dlouho renderování trvá a proč. Je to první nástroj při výkonnostních problémech.

// Simulace: čas renderu bez/s optimalizací

ProductList (bez)
0ms
ProductList (memo)
0ms
VirtualList (100k)
0ms
// React DevTools Profiler API
import { Profiler } from 'react';

function onRenderCallback(
  id,           // "ProductList"
  phase,        // "mount" nebo "update"
  actualTime,   // ms strávených renderem
  baseTime,     // odhadovaný čas bez memoizace
  startTime,
  commitTime
) {
  console.log(`[${id}] ${phase}: ${actualTime.toFixed(2)}ms`);
  // Logovat do monitoring nástroje v produkci
}

<Profiler id="ProductList" onRender={onRenderCallback}>
  <ProductList products={products} />
</Profiler>

// Detekce zbytečných re-renderů
// DevTools → Components → "Highlight updates" checkbox
// Každý záblesk = re-render (žlutá = update, modrá = mount)

// Kvíz: Co je "Wasted render" v React DevTools Profileru?

// 01 / 07Memoizace →

Memoizace — React.memo, useMemo, useCallback

// React.memo — přeskočí re-render pokud props nezměněny
const ProductCard = React.memo(function ProductCard({ product, onAdd }) {
  console.log('ProductCard render:', product.id);
  return (
    <div>
      <h3>{product.nazev}</h3>
      <button onClick={() => onAdd(product.id)}>Přidat</button>
    </div>
  );
}, (prevProps, nextProps) => {
  // Custom comparator — true = props stejné, přeskočit render
  return prevProps.product.id === nextProps.product.id &&
         prevProps.product.cena === nextProps.product.cena;
});

// ❌ Problém: onAdd je nová funkce každý render → memo nefunguje!
function Parent() {
  const [count, setCount] = useState(0);
  const handleAdd = (id) => addToCart(id); // nová reference!
  return <ProductCard onAdd={handleAdd} />;
}

// ✅ useCallback — stabilní reference funkce
function Parent() {
  const [count, setCount] = useState(0);
  const handleAdd = useCallback((id) => {
    addToCart(id);
  }, []); // stabilní reference!
  return <ProductCard onAdd={handleAdd} />;
}

// useMemo — cache drahého výpočtu
function FilteredList({ products, filter, sort }) {
  const processed = useMemo(() => {
    console.log('Přepočítávám...'); // jen při změně závislostí
    return products
      .filter(p => p.kategorie === filter)
      .sort((a, b) => a[sort] - b[sort]);
  }, [products, filter, sort]);

  return <List items={processed} />;
}

Code Splitting & Lazy Loading

// React.lazy + Suspense — dynamický import komponenty
import { lazy, Suspense } from 'react';

// Komponenta se načte jen když je potřeba (lazy chunk)
const AdminPanel    = lazy(() => import('./AdminPanel'));
const ProductEditor = lazy(() => import('./ProductEditor'));
const Chart         = lazy(() => import('./Chart'));

function App() {
  return (
    <Suspense fallback={<div>Načítám...</div>}>
      <Routes>
        <Route path="/admin"   element={<AdminPanel />} />
        <Route path="/editor"  element={<ProductEditor />} />
      </Routes>
    </Suspense>
  );
}

// Next.js dynamic import
import dynamic from 'next/dynamic';

const HeavyChart = dynamic(() => import('./HeavyChart'), {
  loading: () => <p>Načítám graf...</p>,
  ssr: false, // renderovat jen na klientovi
});

// Lazy load images (Intersection Observer)
function LazyImage({ src, alt }) {
  const [loaded, setLoaded] = useState(false);
  const imgRef = useRef(null);

  useEffect(() => {
    const observer = new IntersectionObserver(([entry]) => {
      if (entry.isIntersecting) {
        setLoaded(true);
        observer.disconnect();
      }
    });
    if (imgRef.current) observer.observe(imgRef.current);
    return () => observer.disconnect();
  }, []);

  return (
    <img
      ref={imgRef}
      src={loaded ? src : undefined}
      loading="lazy"
      alt={alt}
    />
  );
}

// Kvíz: Co je "code splitting" a proč ho potřebujeme?

Virtualizace seznamů

Renderování 10 000 řádků tabulky = 10 000 DOM elementů = pomalý prohlížeč. Virtualizace renderuje jen viditelné elementy — výkon O(viewport) místo O(n).

// npm install @tanstack/react-virtual

import { useVirtualizer } from '@tanstack/react-virtual';
import { useRef } from 'react';

function VirtualList({ items }: { items: Product[] }) {
  const parentRef = useRef<HTMLDivElement>(null);

  const virtualizer = useVirtualizer({
    count:        items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 60, // odhadovaná výška řádku
    overscan:     5,         // extra řádků mimo viewport
  });

  return (
    <div
      ref={parentRef}
      style={{ height: '500px', overflow: 'auto' }}
    >
      {/* Kontejner s celkovou výškou všech položek */}
      <div style={{ height: virtualizer.getTotalSize() }}>
        {virtualizer.getVirtualItems().map(virtualRow => (
          <div
            key={virtualRow.key}
            style={{
              position:  'absolute',
              top:       0,
              left:      0,
              width:     '100%',
              height:    `${virtualRow.size}px`,
              transform: `translateY(${virtualRow.start}px)`,
            }}
          >
            {/* Renderuje se jen ~10 položek z 100 000 */}
            <ProductRow product={items[virtualRow.index]} />
          </div>
        ))}
      </div>
    </div>
  );
}

// Pro jednoduché případy: react-window
import { FixedSizeList } from 'react-window';
<FixedSizeList height={500} itemCount={100000} itemSize={60}>
  {({ index, style }) => <Row style={style} data={items[index]} />}
</FixedSizeList>
// 04 / 07Bundle →

Bundle analýza & Tree Shaking

# Bundle vizualizace
$ npm install -D rollup-plugin-visualizer  # Vite
$ npx vite build --mode analyze

# Nebo: webpack-bundle-analyzer
$ npx webpack-bundle-analyzer dist/stats.json

# Co hledat:
# - Duplicitní závislosti (2x React v bundle)
# - Velké knihovny bez tree shaking (lodash, moment.js)
# - Nevyužité ikony (import { IconA } from 'heroicons' → celá knihovna)

# ✅ Tree shaking — importovat jen co potřebujete
import { format }     from 'date-fns';         // jen format (~3kB)
// import moment from 'moment';                // celý moment (~70kB)

import { debounce }   from 'lodash-es';        // ES modules lodash
// import _ from 'lodash';                     // celý lodash (~70kB)

import { ChevronDown } from 'lucide-react';    // jen jedna ikona
// import * as icons from 'lucide-react';      // všechny ikony
// 05 / 07Caching →

Caching strategie

// HTTP Cache-Control
// Statické soubory (content hash v názvu)
Cache-Control: public, max-age=31536000, immutable
// API responses
Cache-Control: public, max-age=300, stale-while-revalidate=60
// Uživatelská data
Cache-Control: private, no-cache

// Service Worker — offline caching
// vite-plugin-pwa nebo next-pwa pro automatické generování
const CACHE = 'v1';
self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE).then(cache => cache.addAll([
      '/', '/offline.html', '/styles.css'
    ]))
  );
});

self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request)
      .then(cached => cached || fetch(event.request))
  );
});

// React Query cache strategie
const { data } = useQuery({
  queryKey:  ['products'],
  queryFn:   fetchProducts,
  staleTime: 5 * 60 * 1000,     // 5 min = čerstvá data
  gcTime:    10 * 60 * 1000,    // 10 min = v paměti
  refetchOnWindowFocus: 'always',
});

// Kvíz: Co znamená stale-while-revalidate v Cache-Control?

// 06 / 07Cvičení →

Cvičení — Performance audit

Proveďte audit React aplikace. Pro každý bod identifikujte problém a navrhněte řešení.

// Najděte výkonnostní problémy:

// 1. Komponenta se re-renderuje příliš často
function ProductList({ products, onDelete, searchTerm }) {
  const filtered = products.filter(p =>
    p.nazev.toLowerCase().includes(searchTerm)
  ); // ← Problém?

  return (
    <div>
      {filtered.map(p =>
        <ProductCard
          key={p.id}
          product={p}
          onDelete={() => onDelete(p.id)} // ← Problém?
        />
      )}
    </div>
  );
}

// 2. Velký bundle
import moment from 'moment'; // 67kB
// Jak nahradit?

// 3. 50 000 položek v listu
function AllOrders({ orders }) {
  return (
    <ul>
      {orders.map(o => <li key={o.id}>{o.id}</li>)}
    </ul>
  ); // ← Problém + řešení?
}

// Nápověda: useMemo, useCallback, React.memo,
// date-fns místo moment, useVirtualizer
🏅

Performance Engineer

Zvládáte React Profiler, memoizaci, code splitting, virtualizaci a caching.

// 07 / 07Taháček →

// Taháček

// Memoizace
const MemoComp = React.memo(Component, compareFn);
const value    = useMemo(() => expensiveCalc(a,b), [a,b]);
const fn       = useCallback((x) => doSomething(x), [dep]);

// Code splitting
const Lazy = lazy(() => import('./Lazy'));
<Suspense fallback={<Spinner/>}><Lazy/></Suspense>

// Next.js dynamic
const Dyn = dynamic(() => import('./Dyn'), { ssr: false });

// Virtualizace
const virt = useVirtualizer({ count: N, getScrollElement, estimateSize: () => 60 });
virt.getVirtualItems() // jen viditelné řádky

// Cache-Control
"public, max-age=31536000, immutable"     // statické soubory
"public, max-age=300, stale-while-revalidate=60" // API
// Lekce 11 dokončenaL12: Architektura →