Inicio / TypeScript / React: Frontend Moderno con TypeScript / Rendimiento y Optimización

Rendimiento y Optimización

React.memo, useMemo, useCallback, lazy loading, virtualización y Profiler.

Avanzado Web
🔒 Solo lectura
📖

Estás en modo lectura

Puedes leer toda la lección, pero para marcar progreso, hacer ejercicios y ganar XP necesitas una cuenta Pro.

Desbloquear por $9/mes

Performance y Optimización

React es rápido por defecto gracias al Virtual DOM, pero aplicaciones grandes pueden sufrir re-renders innecesarios. Esta lección cubre las técnicas clave para optimizar el rendimiento.


¿Cuándo optimizar?

Regla de oro: primero haz que funcione, luego haz que sea rápido. Mide antes de optimizar.

Señales de que necesitas optimizar:

  • Listas con cientos o miles de elementos
  • Inputs que se sienten lentos al escribir
  • Animaciones que no son fluidas (< 60 fps)
  • Re-renders visibles en React DevTools Profiler

React.memo: evitar re-renders de hijos

React.memo envuelve un componente para que solo se re-renderice si sus props cambian:

import { memo } from 'react'

interface ProductCardProps {
  name: string
  price: number
  onBuy: () => void
}

const ProductCard = memo(function ProductCard({ name, price, onBuy }: ProductCardProps) {
  console.log(`Renderizando: ${name}`)
  return (
    <div className="card">
      <h3>{name}</h3>
      <p>${price}</p>
      <button onClick={onBuy}>Comprar</button>
    </div>
  )
})

Custom comparator

const HeavyComponent = memo(MyComponent, (prevProps, nextProps) => {
  // Retorna true si NO debe re-renderizar (props iguales)
  return prevProps.id === nextProps.id && prevProps.name === nextProps.name
})

¿Cuándo usar memo?

  • ✅ Componentes que reciben las mismas props frecuentemente
  • ✅ Componentes costosos de renderizar (listas, gráficos)
  • ❌ Componentes simples y baratos (un <p> o <span>)
  • ❌ Componentes cuyas props cambian en cada render (sin useCallback)

useMemo: cachear cálculos

function SearchResults({ items, query }: { items: Item[]; query: string }) {
  // ✅ Solo recalcula cuando items o query cambian
  const filtered = useMemo(() => {
    return items
      .filter(item => item.name.toLowerCase().includes(query.toLowerCase()))
      .sort((a, b) => a.name.localeCompare(b.name))
  }, [items, query])

  return (
    <ul>
      {filtered.map(item => <li key={item.id}>{item.name}</li>)}
    </ul>
  )
}

useCallback: estabilizar funciones

Sin useCallback, cada render crea una nueva referencia de función, rompiendo memo en los hijos:

function TodoList() {
  const [todos, setTodos] = useState<Todo[]>([])

  // ✅ Referencia estable — no cambia entre renders
  const handleToggle = useCallback((id: number) => {
    setTodos(prev => prev.map(t => t.id === id ? { ...t, done: !t.done } : t))
  }, [])

  const handleDelete = useCallback((id: number) => {
    setTodos(prev => prev.filter(t => t.id !== id))
  }, [])

  return (
    <ul>
      {todos.map(todo => (
        <TodoItem
          key={todo.id}
          todo={todo}
          onToggle={handleToggle}
          onDelete={handleDelete}
        />
      ))}
    </ul>
  )
}

const TodoItem = memo(function TodoItem({ todo, onToggle, onDelete }: TodoItemProps) {
  return (
    <li>
      <span onClick={() => onToggle(todo.id)}>{todo.text}</span>
      <button onClick={() => onDelete(todo.id)}>🗑️</button>
    </li>
  )
})

Lazy loading y Code splitting

Carga componentes solo cuando se necesitan con React.lazy():

import { lazy, Suspense } from 'react'

// El componente se descarga solo cuando se necesita renderizar
const AdminPanel = lazy(() => import('./pages/AdminPanel'))
const Charts = lazy(() => import('./components/Charts'))
const Settings = lazy(() => import('./pages/Settings'))

function App() {
  return (
    <Suspense fallback={<div className="spinner">Cargando...</div>}>
      <Routes>
        <Route path="/admin" element={<AdminPanel />} />
        <Route path="/charts" element={<Charts />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  )
}

Lazy loading condicional

const HeavyEditor = lazy(() => import('./HeavyEditor'))

function Document() {
  const [isEditing, setIsEditing] = useState(false)

  return (
    <div>
      <button onClick={() => setIsEditing(true)}>Editar</button>
      {isEditing && (
        <Suspense fallback={<p>Cargando editor...</p>}>
          <HeavyEditor />
        </Suspense>
      )}
    </div>
  )
}

Virtualización de listas

Para listas con miles de elementos, solo renderizar lo visible:

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

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

  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 40, // altura estimada de cada fila
  })

  return (
    <div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
      <div style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
        {virtualizer.getVirtualItems().map(virtualRow => (
          <div
            key={virtualRow.key}
            style={{
              position: 'absolute',
              top: 0,
              transform: `translateY(${virtualRow.start}px)`,
              height: `${virtualRow.size}px`,
              width: '100%',
            }}
          >
            {items[virtualRow.index]}
          </div>
        ))}
      </div>
    </div>
  )
}

useTransition: priorizar actualizaciones

useTransition marca actualizaciones como no urgentes, permitiendo que la UI siga respondiendo:

import { useState, useTransition } from 'react'

function SearchWithTransition() {
  const [query, setQuery] = useState('')
  const [results, setResults] = useState<string[]>([])
  const [isPending, startTransition] = useTransition()

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const value = e.target.value
    setQuery(value) // Urgente: actualizar el input inmediatamente

    startTransition(() => {
      // No urgente: filtrar la lista puede esperar
      setResults(filterLargeList(value))
    })
  }

  return (
    <div>
      <input value={query} onChange={handleChange} />
      {isPending && <p>Buscando...</p>}
      <ul>
        {results.map((r, i) => <li key={i}>{r}</li>)}
      </ul>
    </div>
  )
}

useDeferredValue

Similar a useTransition pero para valores que recibes como props:

import { useDeferredValue, useMemo } from 'react'

function SlowList({ query }: { query: string }) {
  const deferredQuery = useDeferredValue(query)

  const items = useMemo(() => {
    return generateHugeList().filter(item =>
      item.includes(deferredQuery)
    )
  }, [deferredQuery])

  return (
    <ul style={{ opacity: query !== deferredQuery ? 0.5 : 1 }}>
      {items.map((item, i) => <li key={i}>{item}</li>)}
    </ul>
  )
}

Checklist de optimización

Técnica Cuándo usarla
React.memo Componente se re-renderiza con mismas props
useMemo Cálculo costoso que no cambia frecuentemente
useCallback Función pasada como prop a componente memo
React.lazy Reducir el bundle inicial (code splitting)
Virtualización Listas con 100+ elementos visibles
useTransition Actualizaciones pesadas que bloquean el input
useDeferredValue Retrasar re-render de parte lenta de la UI
key estable Evitar re-montaje innecesario de componentes
Mover estado abajo Localizar estado para reducir scope de re-render

React DevTools Profiler

1. Instala React DevTools (extensión del navegador)
2. Abre la pestaña "Profiler"
3. Haz clic en "Record"
4. Interactúa con la app
5. Detén la grabación
6. Analiza:
   - Qué componentes se re-renderizan
   - Cuánto tiempo toma cada render
   - Por qué se re-renderizó (con "Why did this render?")

Resumen

Concepto Descripción
memo() Evita re-render si props no cambiaron
useMemo() Cachea resultado de cálculo
useCallback() Cachea referencia de función
lazy() + <Suspense> Code splitting, carga bajo demanda
useTransition Marca actualizaciones como no urgentes
useDeferredValue Valor diferido para partes lentas de la UI
Virtualización Solo renderizar elementos visibles en listas grandes
Profiler Medir antes de optimizar
🔒

Ejercicio práctico disponible

Implementa memoización y virtualización

Desbloquear ejercicios
// Implementa memoización y virtualización
// Desbloquea Pro para acceder a este ejercicio
// y ganar +50 XP al completarlo

function ejemplo() {
    // Tu código aquí...
}

¿Te gustó esta lección?

Con Pro puedes marcar progreso, hacer ejercicios, tomar quizzes, ganar XP y obtener tu constancia.

Ver planes desde $9/mes