Rendimiento y optimización en React Native para producción

Capítulo 10

Tiempo estimado de lectura: 9 minutos

+ Ejercicio

Qué significa “rendimiento” en React Native (y dónde se rompe)

En producción, el rendimiento se percibe como fluidez (scroll sin tirones), respuesta inmediata a toques, transiciones suaves y tiempos de carga razonables. En React Native, los cuellos de botella suelen aparecer en tres frentes: JS thread (cálculo y render), UI thread (dibujo/animaciones) y puente/serialización (comunicación entre mundos). La optimización efectiva empieza por medir y aislar el problema, no por “aplicar trucos” al azar.

Métricas observables que debes vigilar

  • FPS UI: caídas indican que la UI thread no llega a pintar a tiempo (jank visual).
  • FPS JS: caídas indican que JS está ocupado (cálculos, renders, reconciliación).
  • Tiempo de render: cuánto tarda un componente/pantalla en renderizar (commit) y cuánto se repite.
  • Bloqueos del JS thread: tareas largas (>16ms) que interrumpen interacción/scroll.
  • Over-render: renders repetidos sin cambios visibles (props iguales, estado derivado, callbacks recreados).

Herramientas de profiling (diagnóstico realista)

1) Monitor de rendimiento (FPS) en React Native

Útil para detectar si el problema es más de UI o de JS. Actívalo desde el menú de desarrollo y observa JS FPS y UI FPS mientras haces scroll y aplicas filtros.

2) React DevTools + Profiler

Permite ver qué componentes renderizan, cuánto tardan y por qué. Úsalo para identificar “culpables” (por ejemplo, una fila de lista que se re-renderiza al cambiar un filtro aunque sus props no cambien).

3) Flipper (React Native plugin)

En entornos de desarrollo, Flipper ayuda a inspeccionar renders, logs, red y a veces plugins de performance. Úsalo para correlacionar acciones (aplicar filtro) con picos de trabajo.

4) Perfilado nativo (Android Studio / Xcode Instruments)

Cuando el problema parece estar en UI thread, layout, imágenes o memoria, el perfilado nativo es clave. En iOS, Instruments (Time Profiler, Core Animation, Allocations). En Android, Android Studio Profiler (CPU, Memory, Frame rendering).

Continúa en nuestra aplicación.
  • Escuche el audio con la pantalla apagada.
  • Obtenga un certificado al finalizar.
  • ¡Más de 5000 cursos para que explores!
O continúa leyendo más abajo...
Download App

Descargar la aplicación

Caso práctico: pantalla con lista y filtros (diagnóstico → hipótesis → mejora → verificación)

Trabajaremos sobre una pantalla típica: un header con filtros (búsqueda, chips de categoría, orden) y una lista de productos. El síntoma: al escribir en el buscador o cambiar un chip, el scroll se vuelve entrecortado y el input se siente “pesado”.

Escenario base (simplificado)

function ProductsScreen() {  const [query, setQuery] = useState('');  const [category, setCategory] = useState('all');  const [sort, setSort] = useState('relevance');  const products = useProducts(); // ya llega en memoria (ejemplo)  const filtered = products    .filter(p => category === 'all' ? true : p.category === category)    .filter(p => p.name.toLowerCase().includes(query.toLowerCase()))    .sort((a, b) => sort === 'price' ? a.price - b.price : 0);  return (    <View style={{ flex: 1 }}>      <Filters        query={query}        onChangeQuery={setQuery}        category={category}        onChangeCategory={setCategory}        sort={sort}        onChangeSort={setSort}      />      <FlatList        data={filtered}        keyExtractor={(item) => item.id}        renderItem={({ item }) => <ProductRow item={item} />}      />    </View>  );}

Aquí hay dos sospechosos comunes: (1) filtrado/orden en cada render (costo en JS) y (2) renderItem recreado + filas no memoizadas (over-render).

1) Diagnóstico

Paso 1: reproduce y mide

  • Activa el monitor de rendimiento y haz scroll mientras escribes en el buscador.
  • En React DevTools Profiler, graba una interacción: escribir 5 caracteres y cambiar un chip.

Paso 2: identifica el patrón

  • Si cae JS FPS al escribir, sospecha de filtrado/orden, renders excesivos o lógica pesada en JS.
  • Si cae UI FPS al hacer scroll, sospecha de celdas pesadas (imágenes), sombras, layout costoso, o demasiadas vistas montadas.
  • En el Profiler, revisa: ¿ProductsScreen se re-renderiza en cada tecla? ¿ProductRow re-renderiza para muchos items aunque solo cambió query?

2) Hipótesis (cuellos de botella típicos en listas con filtros)

  • H1: Cálculo de “filtered” en cada render (filter + sort) bloquea el JS thread.
  • H2: renderItem y callbacks recreados invalidan memoización y fuerzan re-render de filas.
  • H3: ProductRow es costoso (imágenes grandes, layout complejo) y se monta/actualiza demasiado.
  • H4: FlatList no está ajustada (virtualización insuficiente, batch sizes, clipping) para el tamaño real.

3) Mejora (aplicando optimizaciones concretas)

A) Memoización del filtrado/orden (reduce trabajo en JS)

Evita recalcular la lista derivada si no cambian sus dependencias. Además, separa normalizaciones (como toLowerCase) para no repetirlas por item.

const normalizedQuery = useMemo(() => query.trim().toLowerCase(), [query]);const filtered = useMemo(() => {  const byCategory = category === 'all'    ? products    : products.filter(p => p.category === category);  const byQuery = normalizedQuery.length === 0    ? byCategory    : byCategory.filter(p => p.nameLower.includes(normalizedQuery));  if (sort === 'price') {    // copia para no mutar el array original    return [...byQuery].sort((a, b) => a.price - b.price);  }  return byQuery;}, [products, category, normalizedQuery, sort]);

Nota práctica: si puedes, prepara nameLower al construir/normalizar datos (una sola vez) para evitar toLowerCase() por render.

B) Reducir re-render: partición de componentes (separa filtros de lista)

Cuando el usuario escribe, el estado del input cambia muy seguido. Si ese estado vive en el mismo componente que la lista, toda la pantalla re-renderiza. Una estrategia es particionar para que la lista reciba props estables y el header no dispare trabajo extra.

const Filters = React.memo(function Filters({ query, onChangeQuery, category, onChangeCategory, sort, onChangeSort }) {  return (    <View>      <TextInput value={query} onChangeText={onChangeQuery} />      {/* chips y selectores */}    </View>  );});

Luego, asegúrate de que los handlers sean estables:

const onChangeQuery = useCallback((text) => setQuery(text), []);const onChangeCategory = useCallback((c) => setCategory(c), []);const onChangeSort = useCallback((s) => setSort(s), []);

Esto evita que Filters re-renderice por cambios de identidad de funciones (cuando sus props no cambian).

C) Memoización de filas y renderItem estable (evita over-render en la lista)

En listas grandes, el objetivo es que una fila solo re-renderice si cambian sus props relevantes.

const ProductRow = React.memo(function ProductRow({ item, onPress }) {  return (    <Pressable onPress={() => onPress(item.id)}>      <Image source={{ uri: item.imageUrl }} style={{ width: 56, height: 56 }} />      <View>        <Text>{item.name}</Text>        <Text>${item.price}</Text>      </View>    </Pressable>  );});

Haz renderItem estable con useCallback y evita crear funciones inline por fila cuando sea posible:

const onPressProduct = useCallback((id) => {  // navegar o abrir detalle}, []);const renderItem = useCallback(({ item }) => (  <ProductRow item={item} onPress={onPressProduct} />), [onPressProduct]);

Si necesitas props derivadas por item (por ejemplo, “isFavorite”), intenta que provengan de un mapa estable y que solo cambien las filas afectadas.

D) FlatList tuning (virtualización y batch)

Estos ajustes dependen del tipo de celda y del dispositivo. La idea es reducir trabajo por frame y evitar montar demasiadas vistas.

  • keyExtractor estable y único (ya lo tienes con id).
  • initialNumToRender: reduce el costo inicial si la pantalla tarda en aparecer.
  • maxToRenderPerBatch y updateCellsBatchingPeriod: controlan cuántas celdas se renderizan por lote.
  • windowSize: cuántas “pantallas” alrededor del viewport se mantienen montadas.
  • removeClippedSubviews: puede ayudar en Android (prueba y verifica; a veces afecta sombras/overflow).
  • getItemLayout: si las filas tienen altura fija, evita mediciones costosas y mejora scroll.
const ITEM_HEIGHT = 72; <FlatList  data={filtered}  renderItem={renderItem}  keyExtractor={(item) => item.id}  initialNumToRender={10}  maxToRenderPerBatch={10}  updateCellsBatchingPeriod={50}  windowSize={7}  removeClippedSubviews={true}  getItemLayout={(_, index) => ({ length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index })}  keyboardShouldPersistTaps="handled"/>

Guía rápida de ajuste: si el scroll se traba al inicio, baja initialNumToRender. Si aparecen “huecos” al hacer scroll rápido, sube windowSize o maxToRenderPerBatch. Cambia un parámetro por vez y mide.

E) Manejo de imágenes (memoria, decodificación y tamaño)

Las imágenes suelen ser el mayor costo en listas: decodificación, escalado y memoria. Buenas prácticas:

  • Sirve imágenes al tamaño correcto: no cargues una imagen de 2000px para mostrarla a 56px.
  • Evita cambios de tamaño en runtime: define width/height fijos para evitar relayout.
  • Placeholder y carga progresiva: reduce picos de trabajo al entrar a la pantalla.
  • Caché: usa una solución de imagen con caché si tu app lo requiere (especialmente en listas).

Si notas picos de memoria o stutters al cargar imágenes, valida en el profiler nativo (Allocations/Memory) y revisa si hay imágenes demasiado grandes o demasiadas decodificaciones simultáneas.

F) Reducir renders por escritura: debounce y prioridad de actualización

Si filtras en tiempo real, cada tecla puede disparar un filtrado + re-render de lista. Dos enfoques:

  • Debounce del query (esperar 150–300ms antes de aplicar filtro).
  • Separar query “UI” vs query “aplicado” para que el input sea fluido.
function useDebouncedValue(value, delayMs) {  const [debounced, setDebounced] = useState(value);  useEffect(() => {    const id = setTimeout(() => setDebounced(value), delayMs);    return () => clearTimeout(id);  }, [value, delayMs]);  return debounced;}const debouncedQuery = useDebouncedValue(query, 200);const normalizedQuery = useMemo(() => debouncedQuery.trim().toLowerCase(), [debouncedQuery]);

Esto reduce el número de recalculados y commits mientras el usuario escribe rápido.

4) Verificación (comprobar que la mejora es real)

Checklist de verificación con métricas

  • Repite el mismo escenario de prueba (misma lista, mismo gesto, misma secuencia de filtros).
  • En el monitor de rendimiento: ¿subió el JS FPS al escribir? ¿se estabilizó el scroll?
  • En React Profiler: ¿bajó el número de commits? ¿ProductRow dejó de re-renderizar masivamente?
  • En perfilado nativo: ¿bajaron picos de CPU/memoria al entrar a la pantalla o al scrollear?

Tabla de síntomas → causa probable → acción

SíntomaCausa probableAcción recomendada
Input se “traba” al escribirFiltrado/orden pesado en cada teclauseMemo + debounce + precomputar campos
Scroll con tirones pero JS FPS okUI thread saturada (layout/imágenes)Optimizar imágenes, simplificar layout, revisar sombras
Muchas filas re-renderizan al cambiar filtroProps inestables / callbacks recreadosReact.memo + useCallback + evitar inline
Huecos al scroll rápidoVirtualización insuficienteAjustar windowSize/maxToRenderPerBatch
Tiempo inicial alto al abrir pantallaDemasiado render inicialBajar initialNumToRender, skeleton/placeholder

Patrones prácticos para mantener rendimiento en producción

Evita estado derivado que se recalcula sin necesidad

Si un valor se puede derivar de props/estado, calcula con useMemo y dependencias correctas. Si el cálculo es caro, considera moverlo fuera del render (por ejemplo, normalización al recibir datos).

Estabiliza identidades (props) para que memoización funcione

React.memo solo ayuda si las props no cambian de identidad. Objetivo: arrays/objetos/callbacks estables con useMemo/useCallback.

Divide para aislar renders

Particiona: header (inputs) y lista (celdas) con límites claros. Si el header cambia mucho, evita que eso invalide la lista completa.

Optimiza primero lo que se repite más

En listas, una micro-mejora en una fila se multiplica por cientos. Prioriza: filas, imágenes, renderItem, y cálculos por item.

Ahora responde el ejercicio sobre el contenido:

Si al escribir en el buscador de una pantalla con FlatList cae el JS FPS y el input se siente “pesado”, ¿qué acción es más adecuada para mejorar el rendimiento en el JS thread?

¡Tienes razón! Felicitaciones, ahora pasa a la página siguiente.

¡Tú error! Inténtalo de nuevo.

Una caída de JS FPS al escribir suele indicar trabajo pesado en el hilo de JS (filtrado/orden y renders repetidos). Memoizar cálculos derivados y aplicar debounce reduce bloqueos y commits durante la escritura.

Siguiente capítulo

Calidad de código en React Native con testing y buenas prácticas

Arrow Right Icon
Portada de libro electrónico gratuitaReact Native desde Cero a App Profesional
83%

React Native desde Cero a App Profesional

Nuevo curso

12 páginas

Descarga la aplicación para obtener una certificación gratuita y escuchar cursos en segundo plano, incluso con la pantalla apagada.