Zustand vs Redux Toolkit vs Context: Gestores de estado de React según Renders y tamaño del paquete
Una aplicación de carritos construida con ocho patrones de estado de React, evaluada en términos de renders innecesarios y tamaño del paquete comprimido: lo que indican las cifras sobre Zustand y Redux Toolkit.
Fuentes: I Built the Same React App With All 8 State Management Patterns. The Numbers Picked Three Winners.
¿Cuál gestor de estado de React deberías elegir? Esta pregunta vuelve a surgir cada pocos meses y, en su mayoría, depende de las preferencias personales. Obtendrás una respuesta más precisa si construyes la misma función pequeña con cada patrón común, validas el comportamiento con un conjunto de pruebas compartido y cuentas las dos cosas que realmente difieren: con qué frecuencia se vuelven a renderizar los componentes y cuántos kilobytes agrega cada opción. A continuación se presenta ese experimento con ocho patrones, desde Zustand y Redux Toolkit hasta el Contexto simple, las razones por las que aparecen esos números y una guía que puedes volver a ejecutar con tu propio estado.
Cómo se configura el experimento
La aplicación de prueba es deliberadamente sencilla: un carrito de compras con un botón de manzana, un botón de plátano, un total en tiempo real y un distintivo de tema cuyo valor nunca cambia. Cada implementación se somete a la misma prueba de comportamiento, que hace clic en la manzana tres veces y en el plátano dos veces, y verifica que se obtengan resultados idénticos. Mientras se ejecuta la prueba, un contador registra cada renderizado de cada componente. Por separado, esbuild mide cuánto añade cada solución a un paquete minificado y comprimido, excluyendo a React ya que todas las opciones generan el mismo costo.
Los ocho competidores son:
- lifted
useStatetransmitido a través de props - Contexto combinado con
useReducer - Redux Toolkit
- Zustand
- Jotai
- Valtio
- MobX
- Un almacén escrito a mano basado en
useSyncExternalStore, sin dependencias
Todos los ocho cumplen con el contrato común, por lo que las diferencias que haya se refieren al costo y no a la corrección:
✓ lifted useState › passes the shared cart contract
✓ Context + useReducer › passes the shared cart contract
✓ Redux Toolkit › passes the shared cart contract
✓ Zustand › passes the shared cart contract
✓ Jotai › passes the shared cart contract
✓ Valtio › passes the shared cart contract
✓ MobX › passes the shared cart contract
✓ useSyncExternalStore › passes the shared cart contract
Tests 8 passed (8)
Aquí se enumeran las versiones de la biblioteca y el entorno de ejecución utilizados para las mediciones, junto con el repositorio que contiene las ocho implementaciones, las pruebas compartidas y ambos scripts de medición:
Tested with react@19.2.8, zustand@5.0.15, @reduxjs/toolkit@2.12.0, react-redux@9.3.0,
jotai@2.20.2, valtio@2.3.2, mobx@7.0.3, mobx-react-lite@5.0.3, node 22.
Repo: github.com/noorjsdivs/state-patterns — 8 implementations, 1 shared test, 2 measurements.
Dos opciones que mantienen la comparación justa
Estas dos provienen de errores que aparecen en aplicaciones reales, no de las normas de pruebas de rendimiento.
Primero, cada implementación crea su almacenamiento dentro de un componente en lugar de en el ámbito del módulo. De esta manera, cada aplicación iniciada comienza realmente desde cero, y no hay fugas de estado entre ejecuciones de pruebas.
Segundo, la insignia del tema existe únicamente como un elemento pasivo. Se suscribe a un valor que nunca se modifica con ningún clic, por lo que cualquier renderizado que realice durante la prueba es un trabajo inútil cuya culpa solo puede atribuirse al patrón de estado.
Qué está deliberadamente fuera del alcance
El concurso abarca únicamente el estado compartido del cliente. TanStack Query no está incluido porque gestiona un caché de datos del servidor, lo cual es un problema distinto con reglas diferentes. El estado de la URL no está incluido porque la barra de direcciones pertenece al router. Es probable que su aplicación necesite ambos, pero ninguno compite en este evento en particular.
Sorpresa número uno: el tamaño del código es casi idéntico
Antes de los números interesantes, uno aburrido que merece atención. Las ocho implementaciones varían entre 39 y 58 líneas. La opción más extensa, Redux Toolkit con 58 líneas, es solo diecinueve líneas más larga que el enfoque más simple, lifted state con 39 líneas. A esta escala, el argumento sobre el código genérico que domina tantas discusiones sobre gestión de estado se reduce a diecinueve líneas. Las diferencias reales se encuentran en otro lugar y solo se vuelven visibles con herramientas de medición.
El marcador de rendimiento
Aquí se muestra cuántas veces se renderizó cada componente en los cinco clics, incluyendo el montaje inicial:
5 clicks (3 apple, 2 banana) Apple Banana Total Theme(idle)
lifted useState 6 6 6 6
Context + useReducer 6 6 6 6
Redux Toolkit 4 3 6 1
Zustand 4 3 6 1
MobX 4 3 6 1
useSyncExternalStore 4 3 6 1
Jotai 5 4 7 2
Valtio 2 2 2 1
Surgen tres historias distintas de estas columnas.
El estado elevado y Context vuelven a renderizar todo
Con useState elevado y un único Context, cada componente se renderiza con cada clic. La insignia del tema se mostró seis veces aunque su valor nunca cambió, y el botón de plátano se renderizó con cada clic en la manzana. Este es el mecanismo concreto detrás de la advertencia común de que Context no es un gestor de estado. useContext suscribe a un componente al valor completo del contexto, por lo que cada vez que ese valor cambia, todos los componentes que lo consumen se renderizan. Colocar todo tu estado en un único contexto es, en efecto, un estado elevado con una capa adicional.
La solución habitual es dividir el estado en varios contextos o memorizar a los componentes que lo consumen, pero eso no se midió aquí. La fila refleja cómo se escribe Context con mayor frecuencia: un proveedor que alberga un único valor.
Cuatro APIs, un resultado idéntico
Redux Toolkit, Zustand, MobX y el almacén escrito a mano produjeron exactamente la misma columna: cada componente se renderiza una vez al montarse y nuevamente solo cuando realmente cambia el fragmento de estado que lee. Sus APIs no se parecen en nada, pero su comportamiento en tiempo de ejecución es similar, porque las cuatro se basan en la misma idea fundamental. Los componentes escuchan un fragmento específico del estado en lugar de todo el almacén, y solo reciben notificaciones cuando ese fragmento cambia. Para conocer más detalles sobre cómo funciona esa selección y la verificación de igualdad en una de estas bibliotecas, consulte cómo React Redux decide cuándo volver a renderizar.
Los números sospechosamente bajos de Valtio
A primera vista, la columna de Valtio parece un contador roto: dos actualizaciones para un botón que se ha hecho clic tres veces. Las cifras son correctas. Valtio agrupa las notificaciones de cambios en la cola de microtareas, de modo que varias actualizaciones rápidas y síncronas se reducen a una sola actualización por componente, y los valores finales siguen siendo correctos.
Hay una advertencia importante. Una persona que hace clic a velocidad normal genera una actualización por clic, ya que cada clic finaliza antes de que comience el siguiente. La ventaja solo se aprecia cuando las actualizaciones llegan en ráfagas, como ocurre con los mensajes de WebSocket, los datos en streaming o los eventos de arrastre. Si su aplicación tiene ese perfil, esta columna merece una atención detallada.
La actualización adicional constante de Jotai
Jotai renderizó cada componente una vez más que el grupo basado en selectores, incluyendo dos renderizaciones para la insignia del tema inactivo. Su nivel de detalle es correcto, ya que los componentes siguen reaccionando únicamente a los elementos que utilizan, y el renderizado adicional es uniforme en todas las columnas. Ese patrón indica algún problema en la secuencia inicial de montaje con el proveedor Store en lugar de una fuga de suscripciones, pero no se identificó la causa exacta. Considérelo como una pregunta abierta en lugar de un veredicto en contra de Jotai.
El análisis del tamaño del paquete
Esto es lo que cada patrón agrega a un paquete comprimido, contando tanto el código propio del patrón como su biblioteca, tratando a React como un componente externo:
lifted useState 0.4 KB
useSyncExternalStore 0.5 KB (zero dependencies)
Context + useReducer 0.5 KB
Zustand 0.7 KB
Valtio 2.7 KB
Jotai 4.4 KB
Redux Toolkit 10.7 KB
MobX 13.2 KB
La opción más pesada es 33 veces más grande que la más ligera. En otras palabras, Redux Toolkit y MobX juntos suman 23,9 KB, mientras que los seis patrones restantes en conjunto totalizan 9,2 KB.
Lo interesante es cómo esta lista interactúa con la tabla de puntuación de renderizado. Solo dos patrones combinan un comportamiento de renderizado perfecto con un tamaño inferior a un kilobyte: Zustand y el almacén escrito a mano. Redux Toolkit logra el mismo rendimiento en 10,7 KB, y MobX en 13,2 KB. Eso no es necesariamente un inconveniente para ellos; es un costo, y un costo solo tiene sentido una vez que se sabe qué se obtiene a cambio.
Tres opciones prácticas y cuánto cuesta cada una
Para el estado del cliente compartido en una aplicación típica, las mediciones indican tres herramientas que vale la pena considerar.
Zustand como opción por defecto
Zustand ofrece una granularidad de renderizado perfecta con 0,7 KB en 54 líneas, y su API prácticamente no necesita explicaciones para un nuevo colega: se trata de un gancho que acepta un selector. En esta prueba, su rendimiento fue similar al de Redux, pero con aproximadamente una quincuagésima parte del tamaño.
Ese resultado depende de una regla clave: siempre seleccionar una porción específica del estado. Una llamada como useCart(s => s) suscribe al componente a todo el almacén de datos, lo que reproduce el problema del Context. La eficiencia proviene de escribir selectores específicos, no del nombre de la biblioteca. Si un selector devuelve un nuevo objeto o array en cada llamada, también se necesita una función auxiliar para comparaciones superficiales, de lo contrario cada actualización parecerá un cambio real.
Un almacén useSyncExternalStore escrito a mano
Esta opción es la que hace que el experimento sea más atractivo. La implementación completa, incluido el almacén, cabe en 58 líneas, añade 0.5 KB, coincide exactamente con la columna de renderizado de Redux y no presenta dependencias que requieran auditoría o actualización. useSyncExternalStore es la herramienta primitiva que proporciona React mismo para suscribirse a almacenes externos de forma segura, incluso durante el renderizado concurrente. Para los autores de bibliotecas o para equipos que prefieren dependencias mínimas, es suficiente. La contrapartida es que usted es responsable del código: los herramientas de desarrollo, el middleware y la persistencia son algo que debe crear usted mismo si los necesita.
Valtio para actualizaciones intermitentes
El agrupamiento de microtareas de Valtio era medible, real y único entre estos competidores, y 2,7 KB es un precio razonable por ello. Escribir una mutación directa como state.apples++ y ver que se actualizan exactamente los componentes adecuados resulta casi demasiado conveniente, pero los conteos de renderizado confirman que el comportamiento es genuino.
Por qué los demás pierden esta competencia sin ser malos
La forma de la prueba es importante, ya que favorece un estado pequeño y plano. Las otras opciones tienen ventajas que esta aplicación no puede aprovechar:
- Los 10,7 KB de Redux Toolkit se justifican por las herramientas de desarrollo, la depuración mediante viajes en el tiempo, el middleware y las convenciones que funcionan bien en grandes organizaciones. Con cincuenta desarrolladores trabajando en una misma base de código, esas convenciones son el verdadero producto.
- El modelo observable de MobX es el más fuerte en código de dominio con muchas clases, algo que esta aplicación no tiene.
- El gráfico atómico de Jotai brilla cuando el estado derivado se vuelve profundo e interconectado, y un total derivado único apenas lo alcanza.
- El contexto sigue siendo la herramienta adecuada para valores que rara vez cambian, como el tema, la configuración regional o la sesión. Irónicamente, la columna del tema es precisamente donde tuvo el peor rendimiento aquí, ya que el estado del carrito compartía su proveedor.
- El estado elevado sigue siendo la opción correcta para estados que no se comparten. Solo perdió porque este concurso se centra específicamente en el compartir.
Una regla de diseño que no es cuestión de gusto
Cada implementación aquí creó su almacén dentro de un componente. Un almacén definido a nivel de módulo sobrevive al desmontaje, y así es como un carrito de compras sigue funcionando después de cerrar sesión y da la bienvenida a la próxima persona que inicia sesión en el mismo dispositivo. También provoca que el estado se filtre entre pruebas y entre solicitudes durante el renderizado en el servidor. Si solo toma una regla de revisión de código de esta comparación, que sea esta: limite los almacenes al árbol de componentes, generalmente creándolos en un proveedor, a menos que quiera intencionalmente un ciclo de vida global.
Ejecutar el mismo concurso en su propia aplicación
Puede reproducir esta medición para su propio estado en una tarde:
- Elija el elemento de estado compartido más debatido en su aplicación y construya un conjunto de cuatro componentes alrededor de él: dos componentes que escriben, uno que lee un valor derivado y uno que actúa como observador pasivo.
- Agregue un contador de renderizado a cada componente. Un objeto a nivel de módulo más una llamada a
track()en el cuerpo de cada componente requiere alrededor de diez líneas de código. El conteo del “espectador” representa el costo del Context, medido de forma precisa en lugar de estimado. - Mida cada candidato con
esbuild --bundle --minify, indicando que React es un archivo externo. Esto toma unos segundos y agrega una columna con el tamaño en kilobytes a los resultados. - Si está utilizando Context y el “espectador” vuelve a renderizar, divida el contexto o pase a utilizar un almacén basado en selectores, luego ejecute nuevamente el contador e incluya las cifras antes y después en su pull request.
- Repita todo el proceso una vez que React Compiler forme parte de su proceso de compilación, ya que la memorización automática puede modificar significativamente la fila correspondiente al estado compartido.
Preguntas abiertas
Quedan dos hilos sueltos. El más pequeño es la causa de la renderización adicional de Jotai. El más grande es React Compiler. La fila de lifted-state muestra 6/6/6/6 precisamente porque nada en ella está memorizado, y la memorización es exactamente lo que automatiza el compilador. Ver si eso hace que esa fila se alinee con los almacenes basados en selectores en código de producción real, en lugar de en una demostración, es el siguiente experimento obvio.
Qué representan los conteos de renderización y los tamaños de los paquetes
- A pequeña escala, las diferencias en el código genérico entre las bibliotecas de estado son insignificantes; es en el comportamiento de renderización y en el tamaño del paquete donde realmente difieren.
- Un único Contexto que almacena estado cambiante vuelve a renderizar a todos los consumidores, incluidos los componentes que nunca leen el valor modificado.
- Las tiendas basadas en selectores, ya sean Redux Toolkit, Zustand, MobX o una tienda personalizada con
useSyncExternalStore, convergen en el mismo patrón de renderizado eficiente. - Zustand y una tienda escrita a mano ofrecen ese comportamiento con un tamaño inferior a un kilobyte; las bibliotecas más pesadas ganan en tamaño gracias a herramientas y convenciones, no al renderizado.
- El agrupamiento de actualizaciones de Valtio es útil para flujos de actualización intermitentes en lugar de clics normales.
- Cree tiendas dentro del árbol de componentes para que el estado no sobreviva a la sesión que lo generó.
Leer más
- Estado del alcance según su vida útil: cuándo un portal de React realmente necesita un almacén global: Aprenda a decidir dónde debe ubicarse el estado de un portal de React según su propietario y vida útil, por qué los almacenes globales generan errores de limpieza, y en qué casos realmente justifica el uso de Redux o Zustand.
- Estratificación del estado en una aplicación streaming de React: Contexto, Redux Toolkit y RTK Query: Siga el desarrollo de una interfaz de transmisión de video desde el uso de propiedades hasta proveedores anidados, pasando por las slices de Redux Toolkit y RTK Query, y aprenda qué tipo de estado pertenece a cada herramienta.