Rendimiento Móvil
Volver a clases
Desarrollo Móvil●●Intermedio

Rendimiento Móvil

60 min

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

#rendimiento#optimizacion#memo

📚 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.memo compara 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.

jsx
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.

jsx
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.

text
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.

text
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 LecturaIntermedio30 min🔴 Sin IA

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.

jsx
1function 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:

  1. productos tiene 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?
  2. Hay dos objetos y una función que se crean nuevos en cada render de renderItem. Encuéntralos y di qué rompen.
  3. fotoOriginal son fotos de 3 MB tomadas con cámara profesional. ¿Qué pasa al scrollear 500 de ellas?
  4. ¿Qué medirías antes de tocar nada?

Documentación Oficial

DocumentaciónPrincipiante15 min

Documentación de apoyo

Herramientas y guías de medición.