Memoization, useMemo y useCallback

Memoization, useMemo y useCallback

Para comenzar a entender el uso de useMemo y useCallback, es necesario entender lo que es memoization o memoización en español.

¿Qué es memoization?

Memoization es una tecnica de optimización en el codigo para evitar repetir el trabajo computacional previamente realizado. Es por eso que la palabra memo, se refiere a poder recordar un resultado calculado anteriormente.

Tambien se puede entender como la forma de hacer cache de los resultados previos y evitar trabajo innecesario.

Memoization en funciones de Javascript

Cuando se trabaja con funciones en Javascript es fácil de implementar memoization, se puede usar closure o simplemente agregar una propiedad a la función (una función es un objeto). Esta propiedad normalmente puede ser un objeto o un array para guardar los resultados ya calculados.

Voy a escribir un ejemplo, nos quedamos con lo más simple agregando una propiedad memo a la función.

function multiplyByTen(number) {
  const memo = multiplyByTen.memo;
  const memoResult = memo[number];
  
  if (memoResult === undefined) {
    memo[number] = number * 10;
  }
 
  return memo[number];	
}
multiplyByTen.memo = {};

Ahora si ejecutamos varias veces la función con algunas valores para number, los valores anteriormente calculados se extraen de la propiedad memo ahorrándonos de cálculos innecesarios.

multiplyByTen(5); // multiplica 5 * 10 = 50
multiplyByTen(6); // multiplica 6 * 10 = 60
multiplyByTen(5); // No se calcula y se obtiene el valor 50 de la propiedad memo

En el ejemplo expuesto multiplicar por diez es un cálculo sencillo, pero si fuera uno que necesite mucho más poder computacional de lo normal, sí que valdría la pena optimizar la función con memoization.

Memoization con React

En React la optimización con memoization es un poco diferente a lo que acabamos de ver, en React, solo se guarda el valor calculado anteriormente inmediato, y este se actualiza conforme se cambian los parámetros de la función.

Transformar conceptualmente el ejemplo anterior a la forma como React lo implementa, seria como el siguiente ejemplo.

function multiplyByTen(number) {
  const prevInput = multiplyByTen.prevInput;
  
  if (prevInput !== number) {
    multiplyByTen.prevInput = number;
    multiplyByTen.prevResult = number * 10;
  }
 
  return multiplyByTen.prevResult;
}
multiplyByTen.prevResult = null;
multiplyByTen.prevInput = null;

A continuación, al invocar la función con mismo parámetro que el anterior obtiene el valor previo, pero si los parámetros cambian, entonces si se hace el cálculo del resultado y este último se guarda como el prevResult. Así también con el parámetro que se guarda como prevInput.

multiplyByTen(5); // si hace el cálculo para regresar 50
multiplyByTen(5); // No hace el cálculo porque el parametro es el mismo, regresaa 50
multiplyByTen(6); // Si hace el cálculo, regresa 60
multiplyByTen(5); // Se hace el cálculo porque este parametro es diferente al anterior 5 !== 6

React.memo para componentes

En Javascript existen las funciones de alto nivel, en React existen las funciones que generan a los componentes de alto nivel o HOC (High Order Component). Las cuales reciben como parámetro un componente y regresan uno nuevo.

React.memo es una función que genera un HOC como resultado, pero además optimiza el renderizado de este componente HOC.

¿Cómo funciona React.memo?

Ya habíamos comentado que la memoization en React solo guarda los parámetros usados en la última invocación y el resultado anterior. Veamos un ejemplo de cuando se cambia el estado del elemento padre de un elemento creado con React.memo

// Primer render hace el cáculo necesario
<UserInfoMemo firstName="Jaime" lastName="Cervantes"></UserInfoMemo>

// Segundo render, ninguna propiedad cambia, no se hace otro cálculo
<UserInfoMemo firstName="Jaime" lastName="Cervantes"></UserInfoMemo>

// Tercer render, cambia firstName a "Juan", vuelve hacer el cálculo del elemento 
<UserInfoMemo firstName="Juan" lastName="Cervantes"></UserInfoMemo>

// Cuarto render, se vuelve a cambiar firstName a "Jaime", hace otro cálculo del elemento
<UserInfoMemo firstName="Jaime" lastName="Cervantes"></UserInfoMemo>

// Quinto render, se cambia a lastName a "Velasco", hace otro cálculo del elemento
<UserInfoMemo firstName="Jaime" lastName="Velasco"></UserInfoMemo>

// Sexto render, ninguna propiedad cambia, no se hace otro cálculo
<UserInfoMemo firstName="Jaime" lastName="Velasco"></UserInfoMemo>

Ejemplo

El siguiente ejemplo se crea un componente llamado UserInfoMemo con la función memo y otro componente llamado UserInfo sin usar React.memo.

import { useState } from 'react';
import UserInfoMemo from './components/UserInfoMemo';
import UserInfo from './components/UserInfo';

function App() {
  const [title, setTitle] = useState('Memoization');
  const [firstName, setFirstName] = useState('Jaime');
  const [lastName, setLastName] = useState('Cervantes');

  console.log('render App');

  return (
    <div className="App">
      <h1>{title}</h1>
      <UserInfo firstName={firstName} lastName={lastName}></UserInfo>
      <UserInfoMemo firstName={firstName} lastName={lastName}></UserInfoMemo>

      <button onClick={() => setTitle('Nuevo titulo')}>Nuevo titulo</button>
      <button onClick={() => setTitle('Otro nuevo titulo')}>Otro titulo</button>
      <button onClick={() => setFirstName('Juan')}>Nombre a Juan</button>
      <button onClick={() => setFirstName('Jaime')}>Nombre a Jaime</button>
      <button onClick={() => setLastName('Velasco')}>Apellido a Velasco</button>
      <button onClick={() => setLastName('Cervantes')}>Apellido a Cervantes</button>
    </div>
  );
}

export default App;

Cuando cambia el estado de title en App, UserInfo se vuelve a renderizar, pero UserInfoMemo no, este último solo se renderiza cuando su propiedad firstName cambia. Puedes probarlo presionando los botones de título que provocan cambios en el estado title y los botones Juan y Pepe que provocan cambios en el estado de firstName.

A continuación el código de <UserInfo> y <UserInfoMemo>.

components/UserInfo.js

export default function UserInfo({ firstName }) {
  console.log('render');

  return (
    <div>
      <h2>UserInfo</h2>
      <p>firstName: {firstName}</p>
    </div>
  );
}

components/UserInfoMemo.js

import { memo } from "react"

const UserInfoMemo = memo(({ firstName }) => {
  console.log('render with memo');
  
  return (
    <div>
      <h2>UserInfoMemo</h2>
      <p>firstName: {firstName}</p>
    </div>
  );
});

export default UserInfoMemo;

Puedes probar el ejemplo en codesanbox.io.

Edit React.memo

useMemo

El hook useMemo te permite recordar el valor anterior si los parámetros usados para el cálculo no cambian, tal cual implementa el concepto de memoization en React recordando solo el valor anterior.

La sintaxis es la siguiente.

useMemo(
  () => computeExpensiveValue(a, b),
  [a, b]
);

// o así:
useMemo(
  function() {
    return computeExpensiveValue(a, b);
  },
  [a, b]
)

Recibe una función para realizar un cálculo que involucre un gasto considerable de tiempo y recursos computacionales. Y como segundo parámetro una array de dependencias, para que solo se vuelva a ejecutar la función si alguna de las dependencias ha cambiado, de lo contrario regresa el valor recordado previamente.

const memoizedValue = useMemo(expensiveCallback, deps)

Ejemplo sin useMemo

Vamos a suponer que el arreglo de usuarios es muy grande y se obtiene a partir de la petición a un servicio, cada vez que se hace render de <App> se hace la petición y se genera el arreglo cuando cambiamos el estado de selectedUser

import { useState } from "react";
import UserList from "./components/UserList";

function App() {
  const [selectedUser, setSelectedUser] = useState("Jaime Cervantes");
  const users = ["Jaime Cervantes", "Juan Perez", "Carlos Vazquez"];
  
  console.log('render App');

  return (
    <>
      <h1>useMemo</h1>

      <p>Usuario seleccionado: {selectedUser}</p>

      <UserList users={users}></UserList>

      <button onClick={() => setSelectedUser("Pedro Picapiedra")}>
        Cambiar usuario
      </button>
    </>
  );
}

export default App;

Y el componente <UserList>.

import { useEffect } from "react";

export default function UserList({ users }) {
  console.log('render UserList');
  
  useEffect(() => {
    console.log('useEffect UserList');
  }, [users])

  return (
    <ul>
      {
        users.map(name => (
          <li key={name}>
            {name}
          </li>
        ))
      }
    </ul>
  );
}

En el primer render, los console.log se ejecutan como sigue.

render App
render UserList
useEffect UserList

Cuando se cambia el estado de selectedUser provoca otro renderizado de <App> y por consiguiente la asignación de la constante users se vuelve a ejecutar. Dado que se cambió el estado también se vuelve a renderizar <UserList>, hasta ahí no hay problema porque es lo que se espera. El problema es que se vuelve a ejecutar el useEffect en <UserList> debido a que la constante users contiene un nuevo array.

En segundo renderizado por cambio de selectedUser.

render App
render UserList
useEffect UserList

useMemo para users

Lo ideal es que el useEffect no se vuelva a ejecutar, porque los valores de los usuarios en realidad no cambiaron. Aquí es donde useMemo tiene utilidad conservando la referencia del valor anterior.

  const users = useMemo(
    () => ["Jaime Cervantes", "Juan Perez", "Carlos Vazquez"],
    []
  );

De esta manera cuando cambiamos a selectedUser, nuestro useEffect ya no se ejecuta porque se sigue usando el mismo arreglo definido en App y no se vuelve a asignar uno nuevo gracias a useMemo.

render App
render UserList

Usando React.memo sobre <UserList>

Podemos optimizar UserList aún más, convirtiéndolo en un HOC usando la función React.memo.

import { useEffect, memo } from "react";

const UserListMemo = memo(UserList);

export default UserListMemo;

function UserList({ users }) {
  console.log("render UserList");

  useEffect(() => {
    console.log("useEffect UserList");
  }, [users]);

  return (
    <ul>
      {users.map((name) => (
        <li key={name}>
          {name}
        </li>
      ))}
    </ul>
  );
}

Usando React.memo, cuando se presiona el botón para cambiar el userSelected, ya no se renderiza <UserList>. Ahora en la consola solo imprime render App.

render App

Imagínate que users debiera recibir un arreglo con mil usuarios. Vale la pena hacer memoization de esos datos para evitar volver a renderizar <UserList> con mil elementos <li> que representen a cada usuario. Solo es un ejemplo, no es que en aplicaciones reales quieras obtener mil usuarios de una sola vez, para eso están las paginaciones.

Agregando selectUser

El componente UserList necesita establecer el usuario seleccionado. Agregamos una función llamada selectUser que use setUserSelected para actualizar el usuario seleccionado.

import { useState, useMemo } from "react";
import UserList from "./components/UserList";

function App() {
  const [selectedUser, setSelectedUser] = useState("Jaime Cervantes");
  const users = useMemo(
    () => ["Jaime Cervantes", "Juan Perez", "Carlos Vazquez"],
    []
  );

  console.log("render App");

  const selectUser = (user) => setSelectedUser(user);

  return (
    <>
      <h1>useMemo</h1>

      <p>Usuario seleccionado: {selectedUser}</p>

      <UserList users={users} onSelectUser={selectUser}></UserList>
    </>
  );
}

export default App;

Y en <UserList> tenemos lo siguiente.

import { useEffect, memo } from "react";

const UserListMemo = memo(UserList);

export default UserListMemo;

function UserList({ users, onSelectUser }) {
  console.log("render UserList");

  useEffect(() => {
    console.log("useEffect UserList");
  }, [users]);

  return (
    <ul>
      {users.map((name) => (
        <li key={name} onClick={(e) => onSelectUser(name)}>
          {name}
        </li>
      ))}
    </ul>
  );
}

Ahora ya no tenemos problemas con la constante users gracias a useMemo y hasta evitamos el renderizado de <UserList> con React.memo.

useMemo para selectUser

Pero acabamos de agregar una función para actualizar el usuario seleccionado, cada vez que cambiamos el estado de selectedUser, la función se vuelve a crear y al ser una nueva referencia en memoria, entonces la propiedad onSelectUser de <UserList> cambia, por lo que si se renderiza este componente.

render App
render UserList

Para evitar el tema anterior también nos sirve useMemo, al permitirnos guardar un valor de cualquier tipo, también podemos guardar una función, mientras alguna de las dependencias no cambie, la referencia a la función seguirá siendo la misma entre renderizados.

import { useState, useMemo } from "react";
import UserList from "./components/UserList";

function App() {
  const [selectedUser, setSelectedUser] = useState("Jaime Cervantes");
  const users = useMemo(
    () => ["Jaime Cervantes", "Juan Perez", "Carlos Vazquez"],
    []
  );

  console.log("render App");

  const selectUser = (user) => setSelectedUser(user);

  const memoizedSelectUser = useMemo(() => selectUser, []);

  return (
    <>
      <h1>useMemo</h1>

      <p>Usuario seleccionado: {selectedUser}</p>

      <UserList users={users} onSelectUser={memoizedSelectUser}></UserList>
    </>
  );
}

export default App;

Cuando cambiemos el estado de selectedUser, en la consola solo imprimirá lo siguiente.

render App

Puedes ver el código final aquí.

Edit UserList con useMemo

Puntos importantes del código final

  • <UserList> se optimizó con React.memo
  • user y selectUserMemo han sido optimizados con useMemo.
  • Estas dos valores se pasan como propiedades a <UserList>.
  • Los cosole.log en <UserList> (“render UserList” y “useEffect UserList”) solo se ejecutan al primer render porque users y selectUserMemo han sido optimizados con useMemo. Y <UserList> con React.memo.

useCallback

Anteriormente usamos useMemo para guardar la referencia de una función, es algo común que se quiera optimizar la creación de las funciones y evitar renderizados de componentes que reciban como propiedad esa función. Es por eso que existe useCallback, para facilitar la memoization de funciones.

Su sintaxis es como la que sigue.

useCallback(fn, deps);

useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

Es el equivalednte a usar.

useMemo(() => fn(a,b), [a, b)

En el ejemplo que hemos estado trabajando podemos reemplazar useMemo por useCallback.

const memoizedSelectUser = useCallback(selectUser, [])

El código final, finalito, final último de App, es igual al código de abajo y puedes revisarlo aquí

import { useCallback, useMemo, useState } from "react";
import UserList from "./components/UserList";

function App() {
  const [selectedUser, setSelectedUser] = useState("Jaime Cervantes");
  const users = useMemo(
    () => ["Jaime Cervantes", "Juan Perez", "Carlos Vazquez"],
    []
  );

  console.log("render App");

  function selectUser(user) {
    setSelectedUser(user);
  }

  // const memoizedSelectUser =  useMemo(() => selectUser, []);
  const memoizedSelectUser = useCallback(selectUser, []);

  return (
    <>
      <h1>useMemo</h1>

      <p>Usuario seleccionado: {selectedUser}</p>

      <UserList users={users} onSelectUser={memoizedSelectUser}></UserList>

      <button onClick={() => setSelectedUser("Pedro Picapiedra")}>
        Cambiar usuario
      </button>
    </>
  );
}

export default App;

Optimización prematura

Optimización prematura es la raíz de todo mal

Donald Knuth

Aunque estas optimizaciones con React.memo, useMemo y useCallback pueden ser importantes, por ejemplo cuando se renderizan muchos elementos debido a un cambio, como los mil usuarios que comentamos en secciones anteriores en el componente <UserList>. A menudo estas optimizaciones no se deben hacer prematuramente. El consejo de Donald Knuth no se refiere a no hacer optimizaciones por completo, sino hacerlas en el momento adecuado.

¿Pero cuándo es el momento adecuado?

En ambientes ágiles debemos recordar los términos YAGNI y KISS, que se refieren a construir las funcionalidades con el conocimiento que se tiene actualmente y de la forma más simple posible. Aplicando este principio de simplicidad podemos detectar en el futuro posibles optimizaciones, pero solo cuando sean detectadas y tengamos conocimiento que las necesitamos de verdad.

Si nos ponemos a analizar los ejemplos anteriores en realidad la mini aplicación no necesita estas optimizaciones y hacen que nuestro código sea más complejo. Se puede decir que para poder ejemplificar el uso de memoization en React incurrimos en una optimización prematura.

Con esto no quiero decir que no debas refactorizar tu código, claro que si, las pruebas unitarias y funcionales te permiten encontrar puntos de refactorización necesarios que nos ayudan a tener un código más limpio y entendible.

Precisamente el objetivo principal de la refactorización es el entendimiento del código que nos permitan mantener nuestras aplicaciones con el tiempo. Esto muy a menudo resulta en código con mejor rendimiento, pero solo el necesario.

Ya cuando se presente un punto de optimización considerable, donde el beneficio es más grande que tener u código más complejo, por supuesto que se puede implementar.

Conclusiones

React.memo, useMemo y useCallback son útiles a la hora de optimizar código usando la técnica de memoization, solo se recomienda utilizarlos cuando se presentan cualquiera de estores tres puntos.

  • Cálculos que tengan un gasto considerable de tiempo y demás recursos computacionales como la memoria.
  • Guardar la referencia en valores para verificar cuando cambian. Principalmente cuando no son primitivos, como un arreglo, objeto literal o funciones.
  • Evitar renderizados de más cuando los valores que provocan el renderizado en realidad no han cambiado.

Con el uso de esta funciones de memoization en React debemos tomar mucho en cuenta lo que nos aconseja Donald Knuth. Los tres puntos anteriores son la guía para estas optimizaciones.

Optimización prematura es la raíz de todo mal

Donald Knuth

Referencias

https://reactjs.org/docs/hooks-reference.html#usememo

https://reactjs.org/docs/hooks-reference.html#usecallback

https://web.archive.org/web/20130731202547/http://pplab.snu.ac.kr/courses/adv_pl05/papers/p261-knuth.pdf

Deja un comentario

A %d blogueros les gusta esto: