Javascript
Front End
React.js
hooks
performance
La optimización del rendimiento es una consideración importante en las aplicaciones React, especialmente cuando crecen en complejidad. El hook useMemo
es una herramienta poderosa que te permite memorizar cálculos costosos para evitar recomputaciones innecesarias durante los renderizados.
useMemo
es un hook de React que memoriza el resultado de una llamada a función. Solo recalcula el valor memorizado cuando una de sus dependencias ha cambiado, lo que puede mejorar significativamente el rendimiento al evitar cálculos costosos en cada renderizado.
1const valorMemorizado = useMemo(() => calcularValorCostoso(a, b), [a, b]);
En esta sintaxis:
useEffect
)Deberías considerar usar useMemo
cuando:
1import React, { useState, useMemo } from 'react'; 2 3function CalculadoraFactorial() { 4 const [number, setNumber] = useState(1); 5 const [otherState, setOtherState] = useState(false); 6 7 // Este cálculo solo se ejecutará cuando 'number' cambie 8 const factorial = useMemo(() => { 9 console.log('Calculando factorial...'); 10 // Cálculo costoso 11 let result = 1; 12 for (let i = 1; i <= number; i++) { 13 result *= i; 14 } 15 return result; 16 }, [number]); // Solo recalcular cuando number cambia 17 18 return ( 19 <div> 20 <input 21 type="number" 22 value={number} 23 onChange={e => setNumber(parseInt(e.target.value))} 24 /> 25 <p>Factorial de {number} es: {factorial}</p> 26 <button onClick={() => setOtherState(!otherState)}> 27 Cambiar otro estado 28 </button> 29 </div> 30 ); 31}
En este ejemplo:
useMemo
para evitar recalcularlo en cada renderizadonumber
cambiaotherState
cambia, el componente se vuelve a renderizar pero no recalcula el factorialPara entender el beneficio de useMemo
, comparémoslo con un enfoque de variable regular:
1function SinMemo({ list, filter }) { 2 // Esto se ejecuta en CADA renderizado 3 const filteredList = list.filter(item => item.includes(filter)); 4 5 return ( 6 <div> 7 <p>Encontrados {filteredList.length} elementos</p> 8 <ul> 9 {filteredList.map(item => <li key={item}>{item}</li>)} 10 </ul> 11 </div> 12 ); 13}
1function ConMemo({ list, filter }) { 2 // Esto solo se ejecuta cuando list o filter cambian 3 const filteredList = useMemo(() => { 4 return list.filter(item => item.includes(filter)); 5 }, [list, filter]); 6 7 return ( 8 <div> 9 <p>Encontrados {filteredList.length} elementos</p> 10 <ul> 11 {filteredList.map(item => <li key={item}>{item}</li>)} 12 </ul> 13 </div> 14 ); 15}
1function ListaDeProductos({ products, searchTerm, sortOrder }) { 2 const filteredAndSortedProducts = useMemo(() => { 3 // Primero filtrar los productos 4 const filtered = searchTerm 5 ? products.filter(product => 6 product.name.toLowerCase().includes(searchTerm.toLowerCase()) 7 ) 8 : products; 9 10 // Luego ordenarlos 11 return [...filtered].sort((a, b) => { 12 if (sortOrder === 'price') { 13 return a.price - b.price; 14 } else if (sortOrder === 'name') { 15 return a.name.localeCompare(b.name); 16 } 17 return 0; 18 }); 19 }, [products, searchTerm, sortOrder]); 20 21 return ( 22 <div> 23 {filteredAndSortedProducts.map(product => ( 24 <ProductItem key={product.id} product={product} /> 25 ))} 26 </div> 27 ); 28}
1function ComponenteGrafico({ data, width, height }) { 2 // Memorizar objeto de opciones para prevenir renderizados innecesarios 3 const chartOptions = useMemo(() => { 4 return { 5 data, 6 dimensions: { 7 width, 8 height 9 }, 10 margin: { top: 20, right: 30, bottom: 40, left: 50 }, 11 animate: true, 12 colors: generateColorPalette(), // Función costosa 13 legend: buildLegendConfig() // Otra función costosa 14 }; 15 }, [data, width, height]); 16 17 return <Chart options={chartOptions} />; 18}
1function TablaDeDatos({ rawData }) { 2 const processedData = useMemo(() => { 3 // Transformación compleja de datos 4 return rawData.map(item => ({ 5 ...item, 6 fullName: `${item.firstName} ${item.lastName}`, 7 formattedDate: formatDate(item.timestamp), 8 calculatedValue: calculateValue(item.values) 9 })); 10 }, [rawData]); 11 12 return ( 13 <table> 14 <thead> 15 <tr> 16 <th>Nombre</th> 17 <th>Fecha</th> 18 <th>Valor</th> 19 </tr> 20 </thead> 21 <tbody> 22 {processedData.map(item => ( 23 <tr key={item.id}> 24 <td>{item.fullName}</td> 25 <td>{item.formattedDate}</td> 26 <td>{item.calculatedValue}</td> 27 </tr> 28 ))} 29 </tbody> 30 </table> 31 ); 32}
useMemo
funciona bien con React.memo
para optimizar tanto el cálculo como el renderizado:
1// Componente hijo envuelto en React.memo 2const ComponenteCostoso = React.memo(({ data }) => { 3 // Renderizado costoso 4 return <div>{/* Renderizar datos */}</div>; 5}); 6 7function ComponentePadre() { 8 const [count, setCount] = useState(0); 9 const [items, setItems] = useState([1, 2, 3]); 10 11 // Este objeto está memorizado y solo se recrea cuando items cambia 12 const memoizedData = useMemo(() => { 13 return { 14 items, 15 meta: { 16 count: items.length, 17 modified: new Date() 18 } 19 }; 20 }, [items]); 21 22 return ( 23 <div> 24 <button onClick={() => setCount(count + 1)}> 25 Incrementar contador: {count} 26 </button> 27 28 {/* 29 Sin useMemo, se crearía un nuevo objeto data en cada renderizado, 30 causando que ComponenteCostoso se vuelva a renderizar incluso cuando items no ha cambiado 31 */} 32 <ComponenteCostoso data={memoizedData} /> 33 </div> 34 ); 35}
Aunque useMemo
puede mejorar el rendimiento, también tiene costos:
No uses useMemo
para cálculos simples donde la sobrecarga de memorización podría ser más costosa que simplemente recalcular el valor:
1// NO HAGAS ESTO - La sobrecarga no vale la pena 2const valorDuplicado = useMemo(() => valor * 2, [valor]); 3 4// HAZ ESTO EN SU LUGAR 5const valorDuplicado = valor * 2;
También evita usarlo para cada cálculo en tu componente por defecto. Solo añade memorización cuando hayas identificado un problema específico de rendimiento.
Siempre mide el rendimiento antes y después de añadir useMemo
para asegurarte de que realmente está ayudando:
1// Puedes usar console.time para medir 2function Componente({ data }) { 3 const processedData = useMemo(() => { 4 console.time('procesamiento de datos'); 5 const result = operacionCostosa(data); 6 console.timeEnd('procesamiento de datos'); 7 return result; 8 }, [data]); 9 10 // Resto del componente 11}
Al igual que con useEffect
, incluye todos los valores del ámbito del componente que se utilizan dentro de la función de cálculo:
1function ResultadosBusqueda({ query, maxResults, data }) { 2 const results = useMemo(() => { 3 // Los tres valores se usan, así que los tres deberían ser dependencias 4 return data 5 .filter(item => item.name.includes(query)) 6 .slice(0, maxResults); 7 }, [data, query, maxResults]); // Todas las dependencias listadas 8 9 return <ListaResultados results={results} />; 10}
Si tu cálculo memorizado depende de funciones, considera memorizar esas funciones con useCallback
:
1function ListaFiltrable({ items }) { 2 const [filterText, setFilterText] = useState(''); 3 4 // Memorizar la función de filtro 5 const filterFunction = useCallback( 6 (item) => item.name.includes(filterText), 7 [filterText] 8 ); 9 10 // Usar la función memorizada en useMemo 11 const filteredItems = useMemo(() => { 12 return items.filter(filterFunction); 13 }, [items, filterFunction]); 14 15 // Resto del componente 16}
1import React, { useState, useMemo } from 'react'; 2import Chart from './Chart'; 3import DataTable from './DataTable'; 4 5function Dashboard({ rawData }) { 6 const [timeRange, setTimeRange] = useState('week'); 7 const [metric, setMetric] = useState('revenue'); 8 9 // Memorizar lógica de filtrado de tiempo 10 const timeFilteredData = useMemo(() => { 11 const now = new Date(); 12 const timeRanges = { 13 day: 24 * 60 * 60 * 1000, 14 week: 7 * 24 * 60 * 60 * 1000, 15 month: 30 * 24 * 60 * 60 * 1000 16 }; 17 18 const threshold = now.getTime() - timeRanges[timeRange]; 19 20 return rawData.filter(item => 21 new Date(item.timestamp).getTime() > threshold 22 ); 23 }, [rawData, timeRange]); 24 25 // Memorizar cálculos de métricas 26 const metricData = useMemo(() => { 27 // Agrupar por día 28 const grouped = timeFilteredData.reduce((acc, item) => { 29 const date = new Date(item.timestamp).toLocaleDateString(); 30 if (!acc[date]) { 31 acc[date] = { date, value: 0 }; 32 } 33 acc[date].value += item[metric]; 34 return acc; 35 }, {}); 36 37 // Convertir a array y ordenar 38 return Object.values(grouped).sort((a, b) => 39 new Date(a.date) - new Date(b.date) 40 ); 41 }, [timeFilteredData, metric]); 42 43 // Memorizar estadísticas 44 const stats = useMemo(() => { 45 if (metricData.length === 0) return { total: 0, avg: 0, max: 0 }; 46 47 const total = metricData.reduce((sum, item) => sum + item.value, 0); 48 return { 49 total, 50 avg: total / metricData.length, 51 max: Math.max(...metricData.map(item => item.value)) 52 }; 53 }, [metricData]); 54 55 return ( 56 <div className="dashboard"> 57 <div className="controls"> 58 <select 59 value={timeRange} 60 onChange={e => setTimeRange(e.target.value)} 61 > 62 <option value="day">Últimas 24 Horas</option> 63 <option value="week">Últimos 7 Días</option> 64 <option value="month">Últimos 30 Días</option> 65 </select> 66 67 <select 68 value={metric} 69 onChange={e => setMetric(e.target.value)} 70 > 71 <option value="revenue">Ingresos</option> 72 <option value="users">Usuarios</option> 73 <option value="pageviews">Vistas de página</option> 74 </select> 75 </div> 76 77 <div className="summary"> 78 <div className="stat"> 79 <h3>Total {metric}</h3> 80 <p>{stats.total.toLocaleString()}</p> 81 </div> 82 <div className="stat"> 83 <h3>Promedio {metric}</h3> 84 <p>{stats.avg.toLocaleString(undefined, { maximumFractionDigits: 2 })}</p> 85 </div> 86 <div className="stat"> 87 <h3>Máximo {metric}</h3> 88 <p>{stats.max.toLocaleString()}</p> 89 </div> 90 </div> 91 92 <Chart data={metricData} /> 93 94 <DataTable data={timeFilteredData} /> 95 </div> 96 ); 97}
Este ejemplo complejo demuestra varias capas de useMemo
trabajando juntas:
El hook useMemo
es una herramienta poderosa para optimizar el rendimiento de tus aplicaciones React al evitar recálculos innecesarios. Al memorizar cálculos costosos, puedes asegurarte de que solo se ejecuten cuando sus entradas realmente cambien, no en cada renderizado.
Puntos clave a recordar:
useMemo
para cálculos costosos que dependen de estado o propsuseMemo
para cálculos simplesReact.memo
para una optimización completa del renderizadoRecuerda que la optimización prematura puede llevar a código más complejo y difícil de mantener. Usa useMemo
cuando hayas identificado un problema específico de rendimiento, no como un enfoque predeterminado para todos los cálculos en tus componentes.