Material de consulta. Por qué tu app vuela en tu teléfono y se arrastra en el del usuario, y cómo medir antes de optimizar.
Etiquetas
📚 Material de consulta. No ocupa sesión de clase. Está aquí para cuando tu proyecto lo necesite.
El teléfono del usuario no es el tuyo
Desarrollas en una laptop potente, con un emulador que usa su CPU y su memoria, o con un teléfono relativamente nuevo. Tu usuario probable tiene un Android de gama media-baja de hace tres años, con la memoria repartida entre quince apps abiertas.
Esa distancia es la razón de que "en mi máquina funciona" sea especialmente traicionero en móvil. Una app que en tu equipo abre en un segundo puede tardar seis en el de tu usuario — y él no va a pensar "qué teléfono tan lento", va a pensar "qué app tan mala".
La arquitectura, en dos hilos
Como viste en animaciones, tu código corre en el hilo de JavaScript y la interfaz se dibuja en el hilo de UI. Entender esa separación es lo que te permite diagnosticar:
- Si el hilo de JS se satura (cálculos pesados, renders de más), la app deja de responder a toques y las animaciones sin driver nativo se traban.
- Si el hilo de UI se satura (demasiadas vistas, imágenes gigantes, sombras complejas), el scroll va a saltos aunque tu lógica sea perfecta.
El monitor de FPS del cuarto snippet te dice cuál de los dos está sufriendo, y eso decide dónde buscar. Sin ese dato, optimizar es adivinar.
Medir antes de optimizar
Es la regla que más tiempo ahorra y la que menos se sigue. El caso del reto lo ilustra bien: un desarrollador podría pasar una tarde memoizando componentes cuando el problema dominante eran unas imágenes de 3 MB.
Tres herramientas, ninguna requiere instalar nada raro: el monitor de FPS, el Profiler de React DevTools y la pestaña de red. Y la cuarta, que no es una herramienta sino una disciplina: probar en un dispositivo de gama baja real antes de entregar.
Renders innecesarios: memo, useMemo, useCallback
Estas tres herramientas evitan trabajo repetido, pero tienen una condición que casi todos pasan por alto:
React.memocompara las props por referencia. Si le pasas un objeto literal o una función definida en el render, las props siempre son nuevas y la memoización no sirve absolutamente de nada — solo agregaste una comparación extra.
Por eso el primer snippet cambia tres cosas a la vez: estilos fuera del componente, handler con useCallback, y estructuras de datos eficientes (un Set en lugar de includes sobre un array). Memoizar sin arreglar las props es el error más común al "optimizar" React Native.
Y la advertencia opuesta: no memoices por default. Cada useMemo tiene su propio costo y su complejidad de lectura. Se usan donde mediste que hace falta — típicamente filas de listas largas y cálculos sobre arrays grandes.
Listas: lo que sigue después de keyExtractor
Ya sabes que FlatList virtualiza y que keyExtractor es obligatorio. Cuando la lista crece de verdad, el segundo snippet trae la siguiente capa: getItemLayout (si todas las filas miden igual, le ahorras al sistema medirlas una por una — es el que más se nota), y los parámetros de ventana para ajustar cuánto renderiza por lote.
Con una advertencia: esos números son puntos de partida, no una receta. Ajustarlos a ciegas puede empeorar las cosas. Se tocan midiendo.
Las imágenes: el cuello de botella número uno
Si solo vas a optimizar una cosa en tu proyecto, que sea esta. El tercer snippet tiene las cuentas: una foto de 3 MB mostrada en 80×80 se descarga completa, se decodifica en memoria y se reescala. Veinte de ellas visibles pueden acercarse al gigabyte solo en decodificación — y en gama baja, la app se cierra sola.
La solución casi nunca está en el cliente: servir miniaturas desde el servidor resuelve el problema de raíz. Y en el cliente, expo-image te da caché y placeholders sin escribir nada.
El arranque de la app
Lo último que revisa la gente y lo primero que ve el usuario. Dos cosas ayudan más que cualquier micro-optimización: no cargar en el arranque lo que no se ve en la primera pantalla (datos, imágenes, pantallas que aún no visita) y usar la pantalla de carga bien, mostrando contenido en cuanto haya algo — con la caché de la clase de persistencia, tu lista principal puede aparecer al instante mientras se actualiza detrás.
Ejemplos de Código
4 ejemplos
Memoizar bien una fila de lista
Los tres cambios que hacen que React.memo sirva de algo.
1// 1. Estilos FUERA del componente (una sola vez)
2const styles = StyleSheet.create({
3 fila: { padding: 12, marginBottom: 8 },
4});
5
6// 2. El componente memoizado recibe props estables
7const Tarjeta = React.memo(function Tarjeta({ producto, esFavorito, onPress }) {
8 return (
9 <Pressable onPress={() => onPress(producto.id)} style={styles.fila}>
10 {/* ... */}
11 </Pressable>
12 );
13});
14
15function Catalogo({ productos, favoritos }) {
16 // 3. Handler estable + Set para consultas O(1)
17 const abrir = useCallback((id) => navegar("Detalle", { id }), []);
18 const setFavoritos = useMemo(() => new Set(favoritos), [favoritos]);
19
20 const renderItem = useCallback(
21 ({ item }) => (
22 <Tarjeta producto={item} esFavorito={setFavoritos.has(item.id)} onPress={abrir} />
23 ),
24 [setFavoritos, abrir]
25 );
26
27 return <FlatList data={productos} renderItem={renderItem} keyExtractor={(p) => p.id} />;
28}FlatList — más allá de keyExtractor
Props que importan cuando la lista crece de verdad.
1<FlatList
2 data={datos}
3 keyExtractor={(x) => x.id}
4 renderItem={renderItem}
5
6 // Si TODAS las filas miden lo mismo: evita medirlas una por una
7 getItemLayout={(_, index) => ({
8 length: ALTURA_FILA,
9 offset: ALTURA_FILA * index,
10 index,
11 })}
12
13 initialNumToRender={10} // cuántas al primer render
14 maxToRenderPerBatch={10} // cuántas por lote al scrollear
15 windowSize={5} // pantallas de colchón (default 21)
16 removeClippedSubviews // libera las que salen de vista (Android)
17
18 onEndReached={cargarMas}
19 onEndReachedThreshold={0.5}
20/>
21
22⚠️ No copies estos números: son puntos de partida. Ajústalos
23MIDIENDO en un dispositivo real.Las imágenes son el cuello de botella
La optimización con mejor relación esfuerzo/beneficio.
1El problema:
2 Una foto de 3 MB (4000×3000) mostrada en 80×80
3 → se descarga completa
4 → se decodifica en memoria (~48 MB sin comprimir)
5 → se reescala para dibujar 6,400 píxeles
6
7Con 20 filas visibles: casi 1 GB de memoria en decodificación.
8En gama baja, la app se cierra sola.
9
10Qué hacer, en orden de impacto:
111. Servir MINIATURAS desde el servidor (160×160 ≈ unos KB)
122. Dimensiones explícitas en el componente Image
133. Caché (expo-image trae caché y placeholders integrados)
144. Formatos modernos (WebP) si tu backend puede servirlos
15
16Regla: nunca muestres una imagen mucho más grande que su
17espacio en pantalla.Medir antes de optimizar
Sin datos, optimizar es adivinar.
11. MONITOR DE FPS
2 Menú de desarrollo → "Show Perf Monitor"
3 Dos números importan: JS (tu código) y UI (el dibujado).
4 Si JS baja de 60 → el problema es tu lógica/renders.
5 Si UI baja de 60 → el problema es lo que se dibuja.
6
72. REACT DEVTOOLS PROFILER
8 Graba una interacción y mira qué componentes re-renderizaron
9 y cuánto tardaron. Los que se repintan sin cambiar nada son
10 tus candidatos a memo.
11
123. RED
13 Revisa el peso real de lo que descargas. Casi siempre hay una
14 sorpresa (y casi siempre es una imagen).
15
164. DISPOSITIVO REAL DE GAMA BAJA
17 El emulador corre sobre tu laptop: te miente sobre CPU y
18 memoria. Consigue el teléfono más modesto que puedas y prueba
19 ahí antes de entregar.Recursos
2 recursos disponibles
Reto de Lectura
Reto de Lectura — La app que se arrastra en gama baja
Cuatro decisiones que no se notan en un teléfono nuevo y hacen la app inusable en uno de 3,000 pesos.
Esta pantalla de catálogo funciona bien en el teléfono del desarrollador. En un Android de gama baja tarda 6 segundos en abrir y el scroll va a saltos.
jsx1function Catalogo({ productos, favoritos }) { 2 const [busqueda, setBusqueda] = useState(""); 3 4 const filtrados = productos.filter((p) => 5 p.nombre.toLowerCase().includes(busqueda.toLowerCase()) 6 ); 7 8 return ( 9 <FlatList 10 data={filtrados} 11 keyExtractor={(p) => p.id} 12 renderItem={({ item }) => ( 13 <Tarjeta 14 producto={item} 15 esFavorito={favoritos.includes(item.id)} 16 onPress={() => abrirDetalle(item.id)} 17 estilos={{ padding: 12, marginBottom: 8 }} 18 /> 19 )} 20 /> 21 ); 22} 23 24function Tarjeta({ producto, esFavorito, onPress, estilos }) { 25 return ( 26 <Pressable onPress={onPress} style={estilos}> 27 <Image source={{ uri: producto.fotoOriginal }} style={{ width: 80, height: 80 }} /> 28 <Text>{producto.nombre}</Text> 29 </Pressable> 30 ); 31}
Preguntas:
productostiene 500 elementos y el usuario teclea en el buscador. ¿Cuántas veces se recorre el array por cada letra y qué más se recalcula?- Hay dos objetos y una función que se crean nuevos en cada render de
renderItem. Encuéntralos y di qué rompen. fotoOriginalson fotos de 3 MB tomadas con cámara profesional. ¿Qué pasa al scrollear 500 de ellas?- ¿Qué medirías antes de tocar nada?
Documentación Oficial
Documentación de apoyo
Herramientas y guías de medición.
Clase 30 de 31 en la ruta Desarrollo movil