La Filosofía de Diseño de React
/ 20 min read
El Mecanismo de Renderizado de React
¿Alguna vez han experimentado esa sensación de que al escribir en una página web, cada tecla que presionan causa un retraso de medio segundo? ¿O tal vez han hecho clic en “ejecutar” y toda la página se vuelve completamente no interactiva, como si fuera una imagen estática que no responde a ningún clic?
Antiguamente, React tenía un defecto fatal en su renderizado: cuando se ejecutaba setState
, todo el árbol del componente actual ejecutaba la función de renderizado. Incluso usando el DOM virtual, esto desperdiciaba una cantidad considerable de tiempo. Porque algunos componentes, cuyos props
dependientes no habían cambiado, o incluso aquellos que no dependían de ningún props
en absoluto, (por defecto) seguían siendo re-renderizados y luego comparados mediante diff. Para aplicaciones web grandes, este defecto a menudo causaba lag notable.
En comparación, Vue naturalmente sabía de qué valores dependía un componente, por lo que podía re-renderizar con precisión a nivel de componente.
Las Causas del Lag
Todos deberían saber que el renderizado de páginas web del navegador y la ejecución de JavaScript compiten por el mismo hilo.
Cuando la imagen necesaria para el refresco no está lista (la razón más común es el bloqueo por tareas largas de JavaScript), ese frame se descarta, causando la sensación de lag. Para mantener una fluidez básica, las tareas que bloquean el renderizado no pueden durar más de 30ms, siendo ideal mantenerlas bajo 16ms. Usar requestAnimationFrame
puede alinearse efectivamente con el tiempo de refresco, dando más tiempo de ejecución a JavaScript.
En el contexto del desarrollo con React, si los desarrolladores no han optimizado deliberadamente, cuanto más alto en la jerarquía sea el componente modificado, más tiempo de renderizado causará. Porque el algoritmo diff antiguo (stack reconciler) no puede ser interrumpido, una vez que el renderizado comienza, los árboles del DOM virtual nuevo y viejo empiezan a compararse recursivamente de forma síncrona y realizar operaciones DOM. Esta operación recursiva carece de mecanismos de salida, por lo que los componentes grandes fácilmente superan los 16ms, causando lag.
Entonces, ¿cómo logra React comprimir el tiempo de renderizado de componentes grandes a 16ms?
Fiber al Rescate
Desde React 16, React introdujo React Fiber, actualizando simultáneamente el Fiber Reconciler. Su algoritmo central trata cada componente del árbol de componentes como una unidad, ejecutando estas unidades por lotes. Cuando React detecta que no hay suficiente tiempo de ejecución, devuelve el hilo al renderizado, y después de completar el renderizado, continúa ejecutando desde el punto de interrupción. Por lo tanto, Fiber proporciona a React la capacidad de pausar, detener y ajustar prioridades en el renderizado.

Al mismo tiempo, todo el flujo de renderizado de la nueva versión se divide en dos pasos: Render y Commit.
Primero veamos qué es exactamente Fiber. En realidad, es simplemente un objeto JavaScript, y en este contexto solo necesitamos preocuparnos por tres propiedades:
export type Fiber = { // ... return: Fiber | null; child: Fiber | null; sibling: Fiber | null; // ...};
Render es un proceso de recorrido lineal de Fiber, y este proceso es interrumpible, algo que el mecanismo recursivo del algoritmo anterior no podía proporcionar. El algoritmo básico es el siguiente, logrando el efecto de buscar primero nodos hijos, si no los hay buscar nodos hermanos, y después de procesar todos los nodos hermanos, retornar al nivel superior para continuar procesando los nodos hermanos del nivel superior, hasta regresar al nodo raíz, lo cual se considera como finalización.
let root = fiber;let node = fiber;while (true) { // Hacer algo con el nodo if (node.child) { node = node.child; continue; } if (node === root) { return; } while (!node.sibling) { if (!node.return || node.return === root) { return; } node = node.return; } node = node.sibling;}

React programa el Fiber Reconciler a través del Work Loop. Cuando no hay suficiente tiempo, React necesita que JavaScript ceda el hilo para responder a las operaciones del usuario y al renderizado de la página. Este algoritmo de programación inicialmente pretendía depender de la interfaz requestIdleCallback
, pero debido a problemas de compatibilidad del navegador que no se resolvían, React implementó su propio sistema.
Esto trae una inspiración: este pensamiento de división temporal del Work Loop también puede usarse en el renderizado de listas largas de Vue.
Commit es después de un Render completo, recopilar una serie de operaciones en el DOM real y ejecutarlas todas juntas; este paso se ejecuta de forma síncrona. Separar estas dos fases puede prevenir inconsistencias cuando el Render se interrumpe y la UI solo se modifica parcialmente. También debido a que estos dos pasos están separados y el paso de renderizado puede ser interrumpido, anteriormente causaba que métodos como componentWillUpdate
fueran llamados dos veces, por lo que posteriormente estos métodos fueron deprecados.
En este Demo se puede ver la comparación de rendimiento entre los dos mecanismos de renderizado nuevo y viejo, la diferencia es enorme.
Análisis de rendimiento de Stack:

Análisis de rendimiento de Fiber:

Fiber más el algoritmo de Reconciliation mejora enormemente la velocidad de respuesta del sistema, con el costo de una ligera extensión del tiempo total de renderizado, lo que parece ser muy rentable.
La Filosofía de los Componentes Funcionales
El núcleo de Fiber es obtener rápidamente el nuevo DOM virtual resultante del render
, y React en sí mismo abraza activamente la programación funcional, por lo que el resultado es naturalmente una combinación perfecta. Aprovechando la oportunidad de la actualización de Fiber, se introdujeron los Hooks, llevando los componentes funcionales hasta el final.
Componentes de Clase:
class Example extends React.Component { state = { count: 0 };
componentDidMount() { // Métodos del ciclo de vida }
render() { return <div>{this.state.count}</div>; }}
Componentes Funcionales:
function Example() { const [count, setCount] = useState(0);
useEffect(() => { // Reemplaza el ciclo de vida }, []);
return <div>{count}</div>;}
En comparación, las principales ventajas de los componentes funcionales son una sintaxis más concisa, eliminando this
, y proporcionando una base más conveniente para extraer lógica común. Además, las funciones del ciclo de vida se unifican en hooks de efectos secundarios como useEffect
, resolviendo de paso el problema de asimetría de componentWillUpdate
mencionado anteriormente.
La característica de las funciones de renderizado es que deben ser funciones puras: mientras la entrada sea determinada, la salida será determinada e inmutable, ejecutándose 10,000 veces dará el mismo resultado, esto es lo que se llama sin efectos secundarios. Podemos expresar este comportamiento con la fórmula UI = f(state)
.
Esta característica trae un problema ligeramente contraintuitivo: ¿cuál es el resultado del alert
?
export default function Counter({ init }) { const [number, setNumber] = useState(init);
return ( <> <h1>{number}</h1> <button onClick={() => { setNumber(number + 5); alert(number); }} > +5 </button> </> );}
Cuando realmente entiendes los mecanismos subyacentes de React y las características de JavaScript, el resultado es obviamente el valor anterior. En el renderizado actual, debido a las características del ámbito estático de JavaScript, number
es un valor fijo, y después de setNumber
no se re-renderiza inmediatamente (sino que solo entra en la cola esperando que el Work Loop lo programe), por lo que incluso si alert
está después de setNumber
, no obtendrá inmediatamente el nuevo resultado.
Introduciendo los Hooks

Esta imagen es una muy buena metáfora, un Hook es un gancho dentro de un componente funcional. Comparado con las versiones anteriores, después de abrazar completamente los componentes funcionales, todas las propiedades de los componentes de clase anteriores necesitan encontrar una nueva forma de almacenarse. La introducción de los Hooks permite que estas funciones de renderizado mantengan las funcionalidades adicionales de React.
Ya que las funciones puras no pueden registrar estado, esto significa que la esencia de los Hooks es transferir el estado a algún lugar, enganchándose a valores fuera de la función a través del gancho.
Por Qué Deben Estar en el Nivel Superior
Cuando React transfiere el estado, el orden de llamada es crucial. Si se escriben dentro de if
o funciones, el orden de llamada no puede garantizarse en absoluto.
Veamos la implementación de useState
de Didact:
let wipFiber = null;let hookIndex = null; // almacenamiento del orden de hooks
function updateFunctionComponent(fiber) { wipFiber = fiber; hookIndex = 0; wipFiber.hooks = []; const children = [fiber.type(fiber.props)]; reconcileChildren(fiber, children);}
function useState(initial) { const oldHook = wipFiber.alternate && wipFiber.alternate.hooks && wipFiber.alternate.hooks[hookIndex]; // obtener hook según index const hook = { state: oldHook ? oldHook.state : initial, queue: [], };
const actions = oldHook ? oldHook.queue : []; actions.forEach((action) => { hook.state = action(hook.state); });
const setState = (action) => { hook.queue.push(action); wipRoot = { dom: currentRoot.dom, props: currentRoot.props, alternate: currentRoot, }; nextUnitOfWork = wipRoot; deletions = []; };
wipFiber.hooks.push(hook); hookIndex++; // index + 1 para el siguiente hook return [hook.state, setState];}
Optimizaciones Basadas en el Nuevo Paradigma
Para el nuevo paradigma de React con componentes funcionales + Fiber, las nuevas formas de optimización vienen principalmente desde dos ángulos: caché y programación.
Caché
En comparación con los componentes de clase que ejecutan la función render
al actualizarse, después de cambiar a componentes funcionales, toda la función se ejecuta en cada renderizado. Si este componente contiene funciones grandes y operaciones intensivas de CPU en caché, es muy fácil que cause problemas de rendimiento.
Como se mencionó anteriormente, los Hooks transfieren el estado a otros lugares. Además del estado, a veces también es necesario almacenar funciones grandes, funciones dependientes y operaciones intensivas de CPU en caché.
Aquí es donde debemos mencionar useMemo
y useCallback
.
Veamos el Demo:
import React, { useState, useMemo } from "react";
// Una función que calcula el n-ésimo número de Fibonacci (operación intensiva de CPU)const fibonacci = (n) => { if (n <= 1) return n; return fibonacci(n - 1) + fibonacci(n - 2);};
const App = () => { const [num, setNum] = useState(10); // Número ingresado por el usuario const [count, setCount] = useState(0); // Otro estado no relacionado
// Usar useMemo para cachear el resultado del cálculo const fibValueMemo = useMemo(() => { console.log("Calculating Fibonacci..."); return fibonacci(num); }, [num]);
// Sin caché // const fibValue = fibonacci(num)
return ( <div style={{ padding: "20px" }}> <h1>Ejemplo de useMemo cacheando operaciones intensivas de CPU</h1> <div> <label> Calcular el{" "} <input type="number" value={num} onChange={(e) => setNum(parseInt(e.target.value, 10) || 0)} style={{ width: "50px" }} />{" "} -ésimo número de Fibonacci: </label> </div> <p>Resultado: {fibValueMemo}</p> <button onClick={() => setCount(count + 1)}>Número de clics: {count}</button> </div> );};
export default App;
Cuando el parámetro de fibonacci
llega a 38
o más, la UI comenzará a tener lag. Si no usas useMemo
, cada vez que hagas clic en el botón sentirás que la página web te ignora completamente.
useCallback
se usa para cachear funciones, su única función es mantener invariable su referencia.
Veamos el Demo:
import React, { useState, useCallback, memo } from "react";
// Componente hijo, envuelto con memo para evitar re-renderizados innecesariosconst ChildComponent = memo(({ onClick, name }) => { console.log(`Componente ${name} re-renderizado`); return <button onClick={onClick}>Haz clic en mí ({name})</button>;});
const App = () => { const [count, setCount] = useState(0); const [otherState, setOtherState] = useState(0);
// Usar useCallback para cachear la función const handleClickMemo = useCallback(() => { setCount(count + 1); }, [count]);
// Sin usar useCallback const handleClick = () => { setCount(count + 1); };
return ( <div style={{ padding: "20px" }}> <h1>Ejemplo de useCallback</h1> <p>Contador: {count}</p> <p>Otro estado: {otherState}</p>
{/* Función cacheada con useCallback */} <ChildComponent onClick={handleClickMemo} name="Cacheado" />
{/* Función sin usar useCallback */} <ChildComponent onClick={handleClick} name="Sin caché" />
<button onClick={() => setOtherState(otherState + 1)}> Actualizar otro estado </button> </div> );};
export default App;
Cuando hagas clic en el botón “Actualizar otro estado”, notarás que el componente “Sin caché” se re-renderiza, mientras que el componente “Cacheado” no.
React.memo
es un componente de orden superior que realiza una comparación superficial de las props del componente. Si las props no han cambiado, no re-renderiza el componente. Sin embargo, si la función pasada al componente siempre tiene una nueva referencia (como handleClick
), entonces React.memo
no puede cumplir su función. Aquí es donde necesitas useCallback
para mantener la estabilidad de la referencia de la función.
const createOptions = useCallback(() => { return { serverUrl: "https://localhost:1234", roomId: roomId, };}, [roomId]); // ✅ Solo cambia cuando roomId cambia
useEffect(() => { const options = createOptions(); const connection = createConnection(options); connection.connect(); return () => connection.disconnect();}, [createOptions]); // ✅ Solo cambia cuando createOptions cambia// ...
Cuando las funciones se convierten en dependencias, deben ser cacheadas primero. Porque si no se cachean, según las características de los componentes funcionales, cada ejecución es una función completamente nueva, y al verificar si las dependencias han cambiado, siempre será verdadero.
No pienses que la declaración de funciones también consume tiempo y que el caché puede ahorrar esta parte del tiempo. De hecho, el ejemplo anterior es equivalente a:
const fn = () => { return { serverUrl: "https://localhost:1234", roomId: roomId, };};const createOptions = useCallback(fn, [roomId]);
La declaración de fn
es absolutamente inevitable, useCallback
solo devuelve la declaración de función anterior cuando las dependencias no cambian, no te dejes engañar pensando que esta vez no necesitas declararla 😂.
Por supuesto, si tu función no usa ningún state
o prop
, es mejor definirla directamente fuera del componente, entonces realmente ahorrarías tiempo.
Ya que podemos cachear funciones y resultados de ejecución, ¿por qué no pensar en las posibilidades de caché desde una dimensión más alta y cachear directamente todo el componente?
Basándose en la característica de que las funciones de renderizado son funciones puras UI = f(state)
, mientras state
no cambie, el resultado de ejecución de este componente puede reutilizarse de forma segura. React proporciona la función React.memo
para almacenar componentes, evitando así el re-renderizado de un componente cuando los parámetros son los mismos.
const ExpensiveItem = React.memo(({ count }) => { blockMainThread(500); const arr = Array(count).fill(0); return ( <> {arr.map((i) => ( <div>i</div> ))} </> );});
Programación
Las siguientes dos optimizaciones están estrechamente relacionadas con el nuevo mecanismo de renderizado:
useTransition
en pocas palabras, un renderizado usado para una transición. El renderizado activado en startTransition
puede ser interrumpido inmediatamente para ejecutar necesidades de mayor prioridad.
Este Hook esencialmente utiliza el mecanismo interrumpible de Fiber, interrumpiendo directamente el renderizado actual no importante para dar al usuario el contenido que necesita después.
Veamos el Demo:
export default function TabContainer() { const [isPending, startTransition] = useTransition(); const [tab, setTab] = useState("about");
function selectTab(nextTab) { startTransition(() => { setTab(nextTab); }); }
return ( <> <Suspense fallback={<div>Cargando, por favor espere...</div>}> <DelayedComponent /> </Suspense> <TabButton isActive={tab === "about"} onClick={() => selectTab("about")}> About </TabButton> <TabButton isActive={tab === "posts"} onClick={() => selectTab("posts")}> Posts (slow) {isPending ? "loading" : ""} </TabButton> <TabButton isActive={tab === "contact"} onClick={() => selectTab("contact")}> Contact </TabButton> <hr /> {tab === "about" && <AboutTab />} {tab === "posts" && <PostsTab />} {tab === "contact" && <ContactTab />} </> );}
Puedes comentar startTransition
para sentir la mejora dramática en la velocidad de respuesta.
Otro Hook useDeferredValue
en pocas palabras, es un debouncing avanzado especializado de React. useDeferredValue
es programado por React, se agenda cuando el hilo no está ocupado, y se retrasa continuamente cuando está ocupado.
Cuando React detecta que el sistema está ocupado, no activará useDeferredValue
, por lo que este valor solo se actualizará después de que termine el período ocupado, funciona de maravilla en combinación con React.memo
.
Veamos el Demo:
import React, { useState, useDeferredValue, startTransition } from "react";
function blockMainThread(duration) { const start = performance.now(); while (performance.now() - start < duration) {}}
const ExpensiveItem = React.memo(({ count }) => { blockMainThread(500); const arr = Array(count).fill(0); return ( <> {arr.map((i) => ( <div>i</div> ))} </> );});
const App = () => { const [count, setCount] = useState(0); const deferredCouont = useDeferredValue(count); console.log(count, deferredCouont); return ( <> <button onClick={() => setCount(count + 1)}> <span role="img" aria-label="react-emoji"> ⚛️ </span>{" "} {count} </button> <ExpensiveItem count={deferredCouont} /> </> );};
export default App;
Rompiendo la Excepción de las Funciones Puras
Como componentes de renderizado de funciones puras, a veces es inevitable que necesiten producir efectos secundarios, por ejemplo, después del renderizado podrían necesitar solicitar automáticamente interfaces ajax para proporcionar datos a la página. React introduce el Hook useEffect
, donde puedes colocar tus efectos secundarios con tranquilidad.
Las operaciones de efectos secundarios comunes incluyen:
- Obtención de datos (como llamadas a API)
- Suscripción a eventos (como WebSocket o escucha de eventos DOM)
- Modificación manual del DOM (como manipular directamente document)
- Configuración de temporizadores (como setTimeout o setInterval)
- Modificación de variables globales o estado externo
Siendo Responsables de los Efectos Secundarios
Si los efectos secundarios son causados cuando el componente se monta, entonces podríamos tener la responsabilidad de limpiar estos efectos secundarios cuando el componente se destruye.
useEffect(() => { const connection = createConnection(serverUrl, roomId); return () => connection.disconnect();}, [roomId]);
Tal vez hayas escuchado sobre el problema de que useEffect
se ejecuta dos veces, este es un problema viral, causado por el modo estricto que React recomienda usar en el entorno de desarrollo. El modo estricto ejecuta intencionalmente dos veces para exponer si los efectos secundarios de useEffect
se limpian correctamente. Normalmente, las funciones puras deben tener el mismo resultado de ejecución, por lo que ejecutarlas repetidamente no debería causar ningún problema. Si aparecen problemas, es porque el desarrollador no siguió la configuración de React de que el renderizado debe ser una función pura, o porque los efectos secundarios traídos por useEffect
no se limpiaron, causando que el programa no funcione como se esperaba.
Si sientes anomalías inmediatamente al abrir, muy probablemente sea causado por la ejecución repetida del modo estricto. En este momento, lo que debemos hacer no es desactivar el modo estricto, sino considerar si el componente ha introducido efectos secundarios inesperados.
Eventos y Efectos Secundarios
Además de useEffect
, los eventos también pueden ser funciones no puras, porque los eventos no se ejecutan durante el renderizado. La diferencia entre eventos y useEffect
es que: los eventos son causados por operaciones del usuario, mientras que los efectos secundarios causados por useEffect
están fuertemente asociados con el renderizado de React.
useEffect
es una función que tiene la oportunidad de activarse cada vez que el componente se renderiza, si se activa depende de si sus valores de dependencia han cambiado.
La documentación oficial dedica una cantidad muy grande de espacio a explicar situaciones donde los principiantes podrían usar useEffect
, pero en realidad no lo necesitan. Una idea importante aquí es prioridad de eventos, aquí hay un ejemplo simple:
useEffect(() => { setSearch("");}, [currentTeam]);
En el caso de que search
no dependa de currentTeam
, escribir así te dará la advertencia This hook specifies more dependencies than necessary: currentTeam
(usando Biome). En realidad, hacer esto es realmente fabricar relaciones de dependencia de la nada, un mejor método de manejo es realmente restablecer search en el evento que activa currentTeam
en sí.
Hacer esto, además de ser difícil de entender estas relaciones fabricadas durante el mantenimiento posterior del código, también afectará el rendimiento de la aplicación. Cuando currentTeam
cambia, ya se re-renderizará, y luego llamar a setSearch
después del re-renderizado, tendrá que ejecutar render una vez más. Una mejor solución es la siguiente:
<button onClick={() => { setCurrentTeam(item.teamId); setSearch(""); refetch(); }}> {item.name}</button>
En este escenario, useEffect
es un poco similar al watch
de Vue, por la misma lógica, Vue tampoco debería abusar de watch
.
En resumen, usa activamente el manejo basado en eventos. Los problemas que se pueden resolver con eventos, no uses useEffect
. (Vue también)
Incluso si tienes que usar useEffect
, React tiene requisitos adicionales, no escribas dependencias al azar.
Eventos en Efectos Secundarios
Veamos este ejemplo nuevamente, supongamos que un componente necesita reconectar websocket cuando roomId
cambia, pero la función que conecta websocket también necesita usar theme
, si pones theme
en las dependencias, entonces se reconectará innecesariamente cuando cambies el tema.
React introdujo un nuevo Hook useEffectEvent
para el problema de dependencias de useEffect
. Los desarrolladores usan este Hook para abstraer funciones en un “evento de efecto secundario”, convirtiéndose en un evento dedicado para useEffect
.
Tiene los siguientes puntos dignos de atención:
- No es un “valor reactivo”, tampoco puede convertirse en una dependencia
- Sin dependencias, pero siempre puede mantener los valores referenciados como los más recientes
- Según su nombre, debería usarse solo para
useEffect
Este es sin duda un Hook muy difícil de entender para los desarrolladores, después de todo, está dedicado específicamente a resolver el problema de dependencias de useEffect
, esto es un poco como cavar otro hoyo para llenar un hoyo.
Si necesitamos una explicación forzada, podemos entender aproximadamente que: los “eventos” (incluso funciones) no son originalmente elementos que activan cambios, el estado sí lo es, pero ahora React trata todas las funciones como valores reactivos, este diseño tiene problemas desde el principio.
// antesfunction ChatRoom({ roomId, theme }) { useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.on("connected", () => { showNotification("Connected!", theme); }); connection.connect(); return () => connection.disconnect(); }, [roomId, theme]);
return <h1>Welcome to the {roomId} room!</h1>;}
// despuésfunction ChatRoom({ roomId, theme }) { const onConnected = useEffectEvent(() => { showNotification("Connected!", theme); });
useEffect(() => { const connection = createConnection(serverUrl, roomId); connection.on("connected", () => { onConnected(); }); connection.connect(); return () => connection.disconnect(); }, [roomId]);
return <h1>Welcome to the {roomId} room!</h1>;}
Pero dicho todo esto, en realidad useEffectEvent
todavía está en etapa experimental, por lo que este problema ahora todavía necesita ser manejado ignorando las advertencias del linter.

useEffect is your last resort
Recuerda, useEffect
es el último recurso, si puedes usar eventos, ¡prioriza los eventos! Solo considera usarlo cuando realmente no hay eventos que lo activen.
El Futuro de React
JSX basado en componentes funcionales es realmente cada vez más libre, pero el costo es una curva de aprendizaje bastante empinada. Ya sea entender un montón de conceptos de componentes funcionales, manejar problemas de rendimiento, problemas de dependencias de useEffect
, todos estos diseños traerán carga mental a los principiantes.
Pero afortunadamente, el equipo de React ya se ha dado cuenta de este problema. En versiones futuras, forwardRef
será removido, el mecanismo de caché automático del React compiler (incluyendo caché de granularidad fina del contenido de componentes y caché de resultados de ejecución de funciones), es decir, mucho del contenido de optimización mencionado anteriormente ya no necesitará control adicional de los desarrolladores.
React, desde el Fiber de optimización de rendimiento de renderizado al principio, hasta cambiar el paradigma de componentes a componentes funcionales más adecuados para Fiber, y luego lanzar Hooks que permiten a Fiber exprimir el rendimiento, esta evolución realmente ha hecho que los días sean cada vez mejores.
Debería haber más Hooks en el futuro para perfeccionar la filosofía de diseño de React, este camino funcional probablemente se seguirá hasta el final, úsalo temprano y disfruta temprano, úsalo tarde y disfruta la facilidad.