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).
- Escuche el audio con la pantalla apagada.
- Obtenga un certificado al finalizar.
- ¡Más de 5000 cursos para que explores!
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/heightfijos 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íntoma | Causa probable | Acción recomendada |
|---|---|---|
| Input se “traba” al escribir | Filtrado/orden pesado en cada tecla | useMemo + debounce + precomputar campos |
| Scroll con tirones pero JS FPS ok | UI thread saturada (layout/imágenes) | Optimizar imágenes, simplificar layout, revisar sombras |
| Muchas filas re-renderizan al cambiar filtro | Props inestables / callbacks recreados | React.memo + useCallback + evitar inline |
| Huecos al scroll rápido | Virtualización insuficiente | Ajustar windowSize/maxToRenderPerBatch |
| Tiempo inicial alto al abrir pantalla | Demasiado render inicial | Bajar 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.