Overreacted

Antes de utilizar memo()

23 de febrero de 2021 • ☕️ 6 min read

Hay muchos artículos escritos acerca de las optimizaciones de rendimiento de React. En general, si alguna actualización de estado es lenta, deberías:

  1. Verificar que estás corriendo una compilación (build) de producción. (Las compilaciones (builds) de desarrollo son intencionalmente más lentas, incluso, en casos extremos en un orden de magnitud.)
  2. Verificar que no ha puesto el estado más arriba de lo necesario en el árbol. (Por ejemplo, poner la entrada del estado en un almacén centralizado podría no ser la mejor idea)
  3. Ejecutar React DevTools Profiler para ver qué se vuelve a renderizar, y envolver los subárboles más costosos con memo(). (Y agregar useMemo() donde sea necesario.)

Este último paso es molesto, especialmente para los componentes intermedios, e idealmente un compilador lo haría por ti. En el futuro, podría ser.

En esta publicación, quiero compartir dos técnicas diferentes. Son sorprendentemente básicas, razón por la cual las personas rara vez se dan cuenta de que mejoran el rendimiento de renderizado.

¡Estas técnicas son complementarias a lo que ya sabes! No reemplazan memo o useMemo, pero, a menudo son buenas para probarlas primero.

Un Componente Lento (Simulado)

Aquí hay un componente con un problema grave de rendimiento en el renderizado:

import { useState } from 'react';

export default function App() {
  let [color, setColor] = useState('red');
  return (
    <div>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      <p style={{ color }}>Hello, world!</p>
      <ExpensiveTree />
    </div>
  );
}

function ExpensiveTree() {
  let now = performance.now();
  while (performance.now() - now < 100) {
    // Artificial delay -- do nothing for 100ms
  }
  return <p>I am a very slow component tree.</p>;
}

(Pruébalo aquí)

El problema es que cada vez que color cambia dentro de App, volveremos a renderizar <ExpensiveTree /> el cual hemos simulado para ser muy lento.

Podría poner memo() en él y terminarlo, pero existen muchos artículos al respecto, por lo que, no perderé tiempo en ello. Quiero mostrar dos técnicas diferentes.

Solución 1: Mover el Estado Hacia Abajo

Si observas el código de renderizado atentamente, notarás que solo una parte del árbol devuelto realmente se preocupa por el color actual:

export default function App() {
  let [color, setColor] = useState('red');  return (
    <div>
      <input value={color} onChange={(e) => setColor(e.target.value)} />      <p style={{ color }}>Hello, world!</p>      <ExpensiveTree />
    </div>
  );
}

Así que extraigamos esa parte en un componente Form y movamos el estado abajo en él:

export default function App() {
  return (
    <>
      <Form />      <ExpensiveTree />
    </>
  );
}

function Form() {
  let [color, setColor] = useState('red');  return (
    <>
      <input value={color} onChange={(e) => setColor(e.target.value)} />      <p style={{ color }}>Hello, world!</p>    </>
  );
}

(Pruébalo aquí)

Ahora, si el color cambia, solo el Form se vuelve a renderizar. Problema resuelto.

Solución 2: Elevar el Contenido

La solución anterior no funciona si la parte del estado se utiliza en algún lugar por encima del árbol costoso. Por ejemplo, digamos que ponemos el color en el <div> padre:

export default function App() {
  let [color, setColor] = useState('red');  return (
    <div style={{ color }}>      <input value={color} onChange={(e) => setColor(e.target.value)} />
      <p>Hello, world!</p>
      <ExpensiveTree />
    </div>
  );
}

(Pruébalo aquí)

Ahora, parece que no podemos “extraer” simplemente las partes que no utilizan color dentro de otro componente, ya que eso incluiría el div padre, que luego incluiría <ExpensiveTree />. No podemos evitar memo esta vez, ¿cierto?

¿O, sí podemos?

Juega con este sandbox e intenta resolverlo.

La respuesta es notablemente simple:

export default function App() {
  return (
    <ColorPicker>
      <p>Hello, world!</p>      <ExpensiveTree />    </ColorPicker>
  );
}

function ColorPicker({ children }) {  let [color, setColor] = useState("red");
  return (
    <div style={{ color }}>
      <input value={color} onChange={(e) => setColor(e.target.value)} />
      {children}    </div>
  );
}

(Pruébalo aquí)

Separamos el componente App en dos. Las partes que dependen del color, junto con la variable de estado color propiamente, se han movido a ColorPicker.

Las partes a las que no les importa el color se quedan en el componente App y son pasadas a ColorPicker como contenido JSX, también conocido como la propiedad children.

Cuando el color cambia, ColorPicker se vuelve a renderizar. Pero, todavía tiene la misma propiedad children que obtuvo de App la última vez, por lo que React no visita este subárbol.

Y como resultado, <ExpensiveTree /> no se vuelve a renderizar.

¿Cuál es la moraleja?

Antes de aplicar optimizaciones como memo o useMemo, podría tener sentido buscar si se puede separar las partes que cambian de las que no cambian.

La parte interesante de estos enfoques es que en realidad no tienen nada que ver con el rendimiento, per se. Utilizar la propiedad children para separar componentes usualmente hace que el flujo de datos de tu aplicación sea más fácil de seguir y reduce la cantidad de propiedades conectados a través del árbol. La mejora del rendimiento en casos como este es una guinda del pastel, no el objetivo final.

Curiosamente, este patrón también desbloquea más beneficios de rendimiento en el futuro.

Por ejemplo, cuando Los Componentes del Servidor estén estable y listos para su utilización, nuestro componente ColorPicker podría recibir sus children desde el servidor. El componente completo <ExpensiveTree /> o sus partes podrían ejecutarse en el servidor, e incluso una actualización de estado de React de nivel superior “saltaría” esas partes en el cliente.

¡Eso es algo que incluso memo no podría hacer! Pero de nuevo, ambos enfoques son complementarios. No descuides mover el estado hacia abajo (¡y subir el contenido!)

Luego, donde no sea suficiente, utiliza el Profiler y esparce esos memo’s.

¿No leí sobre esto antes?

Si, probablemente.

Esto no es una idea nueva. Es una consecuencia natural del modelo de composición de React. Es lo suficientemente simple como para subestimarlo, y merece un poco más de amor.