Rendimiento y React Profiler | Nicolás Garzón
El objetivo de optimizar React no es conseguir cero renders, sino reducir el trabajo que afecta una interacción real. Un render puede ser barato y no producir ningún cambio del DOM.
El rendimiento de una interfaz depende de varias capas:
Texto
Copiar red y servidor
↓
JavaScript descargado
↓
render de React
↓
commit y DOM
↓
style, layout y paint
↓
respuesta percibidaReact Profiler muestra una parte del sistema. Para comprender la experiencia completa también necesitas las herramientas de rendimiento y red del navegador.
Cuando un componente vuelve a ejecutarse, React calcula un siguiente árbol. Puede concluir que el DOM no necesita cambios.
TypeScript
Copiar function Greeting ( { name } : { name: string } ) {
return < p> Hola, { name} < / p> ;
} Un render del padre puede ejecutar Greeting aunque name sea igual. Eso no significa que el párrafo real haya sido recreado.
Optimiza cuando el trabajo medido sea relevante, no porque un contador aumente.
Un componente puede renderizar cuando:
Actualiza su state.
Renderiza su padre.
Cambia un Context consumido.
Cambia el snapshot de una store externa.
React reintenta trabajo suspendido o concurrente.
memo puede omitir algunos renders por props, pero no evita actualizaciones de state, Context o stores consumidos dentro del componente.
Define primero el escenario:
Abrir un modal.
Escribir en un buscador.
Cambiar un filtro.
Navegar a una ruta.
Añadir un producto.
Mostrar una lista grande.
Después registra qué trabajo domina la latencia.
Evita optimizar una pantalla en reposo cuando el problema ocurre durante scroll o input.
El Profiler permite observar commits y componentes que participaron.
Ejecuta un build representativo cuando sea posible.
Inicia una grabación.
Realiza una sola interacción.
Detén la grabación.
Identifica el commit lento.
Revisa por qué renderizaron los componentes.
Cambia una causa.
Vuelve a medir.
La duración en desarrollo puede incluir Strict Mode, warnings y tooling. No la tomes como equivalente exacto a producción.
State colocado demasiado alto obliga a recalcular una región grande:
TypeScript
Copiar function App ( ) {
const [ search, setSearch] = useState ( "" ) ;
return (
< >
< SearchInput value= { search} onChange= { setSearch} / >
< LargeDashboard / >
< / >
) ;
} Si LargeDashboard no necesita search, considera una frontera que mantenga el state cerca del input o composición con children.
TypeScript
Copiar function SearchPanel ( ) {
const [ search, setSearch] = useState ( "" ) ;
return < SearchInput value= { search} onChange= { setSearch} / > ;
} No muevas state hacia abajo si varias regiones necesitan una única fuente de verdad. Colocation es un criterio, no una regla absoluta.
TypeScript
Copiar function MovablePanel ( { children } : { children: React. ReactNode } ) {
const [ position, setPosition] = useState ( { x: 0 , y: 0 } ) ;
return (
< div style= { { transform: ` translate( ${ position. x} px, ${ position. y} px) ` } } >
{ children}
< / div>
) ;
} El padre puede crear children una vez y React puede reutilizar esa referencia mientras MovablePanel actualiza su state. La composición a veces evita recalcular descendientes sin memoización manual.
TypeScript
Copiar const ProductRow = memo ( function ProductRow ( { product } : ProductRowProps) {
return < li> { product. name} < / li> ;
} ) ; memo compara props mediante Object.is por defecto. Puede saltar el render cuando las props conservan identidad.
Que el componente nunca renderice.
Que la comparación sea gratuita.
Que Context no lo actualice.
Que un custom comparator sea correcto.
Que la experiencia mejore.
Úsalo cuando el componente renderiza con frecuencia, tiene trabajo relevante y suele recibir props iguales.
TypeScript
Copiar < ProductList filters= { { category, activeOnly } } / > El objeto es nuevo en cada render, por lo que rompe una comparación superficial de memo.
Eso no significa que crear objetos sea malo. Solo importa si una optimización depende de conservar la referencia.
TypeScript
Copiar const filters = useMemo (
( ) => ( { category, activeOnly } ) ,
[ category, activeOnly] ,
) ; No añadas useMemo si ProductList no está memoizado, el cálculo es trivial y no existe otro requisito de identidad.
useMemo almacena el resultado de un cálculo entre renders mientras sus dependencias sean iguales:
TypeScript
Copiar const visibleProducts = useMemo (
( ) => expensiveFilter ( products, filters) ,
[ products, filters] ,
) ;
Cálculos realmente costosos medidos.
Conservar una referencia necesaria para otra optimización.
Evitar recrear un valor que participa en una API sensible a identidad.
No lo uses como garantía semántica. React puede descartar caches en ciertos escenarios; el código debe seguir siendo correcto sin ellas.
TypeScript
Copiar const handleSelect = useCallback ( ( productId: string ) => {
setSelectedId ( productId) ;
} , [ ] ) ; useCallback conserva la función, no evita crear todo el concepto de closure ni hace más rápido su cuerpo.
Se pasa a un hijo memoizado y suele ser la única prop inestable.
Es dependencia de otro hook y cambiarla provocaría sincronización innecesaria.
Una API externa requiere identidad estable.
Toda función no necesita useCallback.
Comparaciones.
Dependencias que mantener.
Memoria.
Complejidad de lectura.
Riesgo de stale closures si el array es incorrecto.
Una optimización que ahorra microsegundos y dificulta el código puede ser un retroceso.
React Compiler puede aplicar memoización automática a componentes y expresiones compatibles, reduciendo la necesidad de memo, useMemo y useCallback manuales.
En el ecosistema actual se utiliza mediante tooling compatible y requiere que el código respete las Rules of React.
No elimina la necesidad de:
Medir.
Colocar state correctamente.
Evitar effects innecesarios.
Diseñar Contexts estrechos.
Reducir bundles y waterfalls.
Virtualizar colecciones grandes.
Mantén memoización manual cuando exista una razón semántica o una integración que la necesite, y verifica la configuración concreta del proyecto.
Cuando cambia un provider, los consumers actualizan:
TypeScript
Copiar < AppContext value= { { user, theme, notifications } } > Un contexto amplio puede propagar cambios no relacionados.
Separar contextos por responsabilidad.
Mantener providers cerca de su feature.
Dividir datos y acciones.
Utilizar una store con selectores para actualizaciones frecuentes.
No memoices todo el árbol para ocultar un Context mal diseñado.
useSyncExternalStore permite que React lea snapshots consistentes y se suscriba a una store externa.
Stores con selectores pueden actualizar solo consumidores de la parte modificada. Esto aporta en state compartido y frecuente, pero añade una dependencia y un modelo adicional.
Empieza con state local, lifting state o Context cuando resuelvan el problema.
Renderizar miles de filas puede afectar JavaScript, DOM, layout y memoria.
Paginación.
Infinite loading.
Virtualización.
Simplificar cada fila.
Evitar mediciones repetidas.
Mantener keys estables.
Virtualización renderiza una ventana visible y elementos de buffer. Debe conservar:
Navegación por teclado.
Lectura accesible.
Alturas o mediciones correctas.
Posición de scroll.
No virtualices una lista pequeña por anticipación.
TypeScript
Copiar const chartData = transformThousandsOfPoints ( points) ;
¿Puede calcularse en el servidor?
¿Puede prepararse al recibir los datos?
¿Puede reducirse la cantidad de datos?
¿Debe ejecutarse por cada pulsación?
¿Necesita Web Worker?
useMemo evita repeticiones bajo dependencias iguales, pero no hace más barato el cálculo inicial ni lo saca del main thread.
Un input debe actualizarse con prioridad:
TypeScript
Copiar const [ query, setQuery] = useState ( "" ) ;
const deferredQuery = useDeferredValue ( query) ; La UI del input usa query; la lista pesada utiliza deferredQuery.
Esto permite que React posponga el árbol costoso. No reduce el coste total y no sustituye un debounce de red cuando quieres evitar requests.
TypeScript
Copiar useEffect ( ( ) => {
setFilteredProducts ( filterProducts ( products, query) ) ;
} , [ products, query] ) ; Este effect produce un render adicional. Calcula durante render o memoiza solo si el cálculo lo requiere.
Las cadenas de effects suelen ser una fuente de trabajo y estados intermedios evitables.
TypeScript
Copiar const AnalyticsPage = lazy ( ( ) => import ( "./AnalyticsPage" ) ) ; Divide código por rutas y funcionalidades grandes poco frecuentes.
Tamaño del chunk.
Probabilidad de uso.
Prefetch.
Waterfalls.
Costo de la dependencia.
Muchos chunks diminutos pueden empeorar la navegación.
APIs lentas.
Consultas N+1.
Requests duplicados.
Imágenes sin optimizar.
Falta de caché.
Waterfalls de rutas y componentes.
Payloads excesivos.
Una mejora de arquitectura de datos puede superar cualquier memoización del render.
Un commit pequeño puede causar layout costoso si modifica propiedades que afectan geometría en muchos nodos.
Usa las herramientas del navegador para observar:
Long tasks.
Recalculate Style.
Layout.
Paint.
Cumulative Layout Shift.
Event timing.
React Profiler no reemplaza este análisis.
Mide con un build de producción y dispositivos representativos:
CPU móvil.
Red lenta.
Dataset realista.
Navegador objetivo.
Extensiones desactivadas cuando interfieran.
El modo desarrollo prioriza diagnósticos, no rendimiento final.
Las métricas de experiencia pueden señalar problemas de carga, respuesta y estabilidad visual. Relaciónalas con flujos concretos; una media global puede ocultar usuarios afectados.
React es solo una parte de esas métricas.
Interpretar cada render como problema.
Añadir useCallback a toda función.
Memoizar cálculos triviales.
Crear comparadores profundos más costosos que el render.
Perfilar únicamente en desarrollo.
Ignorar red, imágenes, layout y servidor.
Virtualizar colecciones pequeñas.
Usar transition esperando que el trabajo termine antes.
Optimizar sin volver a medir.
Sacrificar accesibilidad o claridad por una microoptimización.
Define la interacción lenta.
Mide en un entorno representativo.
Separa red, JavaScript, React y navegador.
Encuentra el trabajo dominante.
Corrige arquitectura antes de microoptimizar.
Aplica una mejora.
Repite la medición.
Conserva el cambio solo si mejora la experiencia.
Render no equivale a commit ni a cambio del DOM.
Memoización es una optimización, no una regla de corrección.
Colocation de state y Contexts estrechos suelen evitar trabajo amplio.
useMemo, useCallback y memo tienen coste y dependencias.
React Compiler automatiza parte de la memoización, no toda la arquitectura de rendimiento.
Listas grandes, red, bundles y layout suelen dominar problemas reales.
Mide una interacción de producción antes y después.
¿Por qué un render adicional puede no importar?
¿Cuándo aporta useCallback?
¿Qué no resuelve useMemo en un cálculo pesado?
¿Por qué React Profiler no basta para diagnosticar toda la experiencia?
¿Qué debes revisar antes de memoizar una lista lenta?
Ver respuestas
Porque puede ser barato y no producir cambios del DOM.
Cuando una integración o un hijo memoizado necesita identidad estable y se ha medido el beneficio.
No reduce el coste inicial ni mueve el trabajo fuera del main thread.
Porque red, layout, paint, imágenes y servidor también afectan la interacción.
Cantidad de elementos, virtualización, state placement, keys, trabajo por fila y datos recibidos.
Seguridad en interfaces React revisa qué riesgos continúan existiendo aunque React escape texto por defecto y cómo proteger los límites cliente-servidor.