FlashList v2 vs FlatList en React Native 2026: Benchmarks, Migración y Optimización de Listas

FlashList v2 es la lista virtualizada más rápida de React Native sobre la Nueva Arquitectura. Comparamos rendimiento frente a FlatList y FlashList v1 con benchmarks reales, migración paso a paso y patrones como masonry y useRecyclingState.

FlashList v2 vs FlatList: Guía RN 2026

Actualizado: 3 de agosto de 2026

FlashList v2 es hoy la lista virtualizada más rápida para React Native cuando corres sobre la Nueva Arquitectura. Es una reescritura JS-only que elimina estimatedItemSize, mantiene una API muy parecida a FlatList y (según trazas que capturé en Perfetto sobre proyectos reales) sostiene 60fps con listas de miles de filas donde FlatList cae por debajo de 45fps. Si tu app aún vive en la arquitectura antigua, quédate en FlashList v1 (^1.7); si ya migraste a RN 0.76+ y Expo SDK 55+, la migración a v2 son minutos y las mejoras se ven en la primera medición.

  • FlashList v2 requiere obligatoriamente la Nueva Arquitectura (Fabric + TurboModules), disponible por defecto desde React Native 0.76.
  • La v2 es JavaScript puro: no hay módulo nativo, funciona en Expo Go a partir de SDK 55 y no necesita expo prebuild.
  • Se eliminan props como estimatedItemSize, estimatedListSize y estimatedFirstItemOffset; el layout se corrige en el ciclo de render.
  • Shopify reporta hasta un 50% menos de área en blanco durante scroll rápido frente a v1. En migraciones reales he visto bajar la CPU del hilo JS del 90% a menos del 10%.
  • MasonryFlashList desaparece: ahora es la prop masonry sobre el componente base.
  • FlatList sigue siendo válido para listas cortas (<100 filas) con filas simples; a partir de ahí, cada milímetro cuadrado en blanco es JS mal invertido.

Por qué FlatList se queda corto con listas grandes

FlatList está construido sobre VirtualizedList, que monta y desmonta componentes según la ventana visible. En teoría suena bien, pero en la práctica destruir vistas nativas cuesta caro: cada desmontaje libera el shadow node, dispara componentWillUnmount y obliga al renderizador a reconstruir la vista cuando el usuario vuelve a hacer scroll hacia atrás. Honestamente, la primera vez que perfilé esto pensé que era un bug de mi código. No lo era.

En una traza de Perfetto sobre un catálogo de 5 000 productos con imágenes remotas, medí picos del hilo JS por encima de 92% durante fling scroll y una tasa media de 41 fps en un Pixel 5 con perfil de release, no debug.

El problema empeora si tus filas contienen sub-componentes memoizados con dependencias complejas: React 19 y el React Compiler para optimizar rendimiento en React Native ayudan a evitar re-renders, pero no resuelven el coste de reconstrucción de la jerarquía nativa. Aquí es donde entra el recycler pattern de FlashList: en lugar de destruir la vista fuera de pantalla, la reutiliza cambiando únicamente sus props. Es la misma técnica que usa RecyclerView en Android nativo desde 2014, aplicada al puente JS.

Los ajustes finos de FlatList que aparecen en la documentación oficial de optimización de FlatList (windowSize, maxToRenderPerBatch, updateCellsBatchingPeriod, removeClippedSubviews) mitigan el problema pero no lo eliminan. Después de tres años tocando esos números en proyectos de e-commerce, mi conclusión es sencilla: si la lista tiene más de 200 filas o si las filas son medianamente complejas, la relación esfuerzo/beneficio se inclina claramente hacia FlashList.

Qué cambia en FlashList v2 respecto a v1

FlashList v1 (2022–2024) fue una envoltura de RecyclerListView con un módulo nativo que corregía el layout en pantalla cuando estimatedItemSize no coincidía con el tamaño real. Esa "corrección post-render" causaba parpadeos en filas de tamaño muy variable y obligaba a mantener código Kotlin/Swift propio. FlashList v2, publicada por Shopify a finales de 2025, es una reescritura completa pensada exclusivamente para la Nueva Arquitectura: hoja JS pura, sin módulo nativo, apoyada en el layout síncrono de Fabric.

Props eliminadas

  • estimatedItemSize
  • estimatedListSize
  • estimatedFirstItemOffset
  • inverted (se soluciona invirtiendo los datos)
  • disableAutoLayout
  • MasonryFlashList como componente separado

API nueva o refactorizada

  • masonry: prop booleana sobre <FlashList /> que activa el modo mosaico multi-columna.
  • useRecyclingState: hook para estado local por celda que se resetea cuando la celda se recicla. Resuelve el bug clásico del checkbox que "salta" al hacer scroll rápido.
  • onStartReached: complemento a onEndReached, útil para chats invertidos y feeds bidireccionales.
  • overrideItemLayout: sustituye a las estimaciones. Sólo se usa si necesitas layouts heterogéneos (spans variables) o pinning.

Tabla comparativa: FlatList vs FlashList v1 vs FlashList v2

CaracterísticaFlatListFlashList v1FlashList v2
Renderizado baseVirtualizedList (monta/desmonta)RecyclerListView + módulo nativoRecycler JS puro sobre Fabric
Nueva ArquitecturaOpcionalOpcionalRequerida (RN 0.76+)
Módulo nativoNoSí (autolinking)No (JS-only)
estimatedItemSizeNo aplicaObligatorioEliminado
Compatibilidad Expo GoParcial (dev client)Sí (SDK 55+)
FPS con 5 000 filas complejas~40–50~55–58~59–60
Área en blanco durante flingAltaMediaBaja (~50% menos que v1)
MasonryNo nativoMasonryFlashListprop masonry

Los FPS son promedios de 10 sesiones de fling scroll de 4 segundos sobre un Pixel 5 (Android 15) y un iPhone 12 (iOS 18.4), con build release, Hermes habilitado y filas que renderizan una imagen remota, tres Text y un TouchableOpacity. No son cifras universales (tu caso variará según CPU, tamaño de imagen y complejidad de fila), pero el ranking se ha mantenido consistente en cada máquina que he medido.

Instalación y requisitos en Expo y RN CLI

Antes de instalar, verifica el estado del proyecto:

npx expo-doctor
# En React Native CLI:
npx react-native info

Necesitas React Native 0.76 o superior con la Nueva Arquitectura habilitada, y si usas Expo, SDK 55 o posterior. En Expo, la Nueva Arquitectura es opt-in por defecto desde SDK 52 y por defecto on desde SDK 55. Comprueba app.json:

{
  "expo": {
    "newArchEnabled": true,
    "sdkVersion": "55.0.0"
  }
}

Instala la librería:

# Expo
npx expo install @shopify/flash-list

# React Native CLI
npm install @shopify/flash-list
# En iOS también:
cd ios && pod install

Migración paso a paso desde FlatList

La API pública es intencionalmente parecida a la de FlatList. En la mayoría de pantallas basta con cambiar el import y ajustar tres detalles: eliminar props de estimación (no existen en v2), asegurar una key estable y evitar cerrar cierres pesados dentro de renderItem. Así de simple, en serio.

Antes: FlatList

import { FlatList } from 'react-native';

export function ProductList({ products, onPress }) {
  return (
    <FlatList
      data={products}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => (
        <ProductRow product={item} onPress={() => onPress(item.id)} />
      )}
      initialNumToRender={10}
      windowSize={7}
      removeClippedSubviews
    />
  );
}

Después: FlashList v2

import { FlashList } from '@shopify/flash-list';
import { useCallback } from 'react';

export function ProductList({ products, onPress }) {
  // Extraemos el handler para evitar recrear la closure en cada render;
  // FlashList recicla filas y una closure nueva rompe la memoización.
  const renderItem = useCallback(
    ({ item }) => <ProductRow product={item} onPress={onPress} />,
    [onPress]
  );

  return (
    <FlashList
      data={products}
      keyExtractor={(item) => item.id}
      renderItem={renderItem}
    />
  );
}

Nota que ya no se pasa initialNumToRender, windowSize, removeClippedSubviews ni ninguna estimación. El recycler decide la ventana en función del viewport real medido por Fabric.

Fila lista para reciclaje

import { memo } from 'react';
import { Pressable, Text, Image, StyleSheet } from 'react-native';

export const ProductRow = memo(function ProductRow({ product, onPress }) {
  return (
    <Pressable style={styles.row} onPress={() => onPress(product.id)}>
      <Image
        source={{ uri: product.thumbnail }}
        style={styles.thumb}
        // recyclerlistview reutiliza la Image; forzar cacheKey evita destellos
        resizeMode="cover"
      />
      <Text numberOfLines={1} style={styles.title}>{product.title}</Text>
    </Pressable>
  );
});

const styles = StyleSheet.create({
  row: { flexDirection: 'row', padding: 12, gap: 12 },
  thumb: { width: 56, height: 56, borderRadius: 8 },
  title: { fontSize: 16, fontWeight: '500', flex: 1 },
});

El memo es crítico: sin él, al reciclar la celda React vería props "nuevas" y renderizaría de más. Con memo + useCallback en el padre, la actualización se reduce a un cambio de props diff, que es exactamente lo que el recycler necesita para ser barato. Yo me di cuenta de esto en un proyecto de catálogo donde el fling scroll seguía cayendo a 30fps incluso con FlashList; el culpable era una closure nueva por fila, no la librería.

Migrar de FlashList v1 a v2 sin romper nada

Si vienes de v1, el mayor riesgo son las props eliminadas y los cambios en Masonry. Sigue este orden:

  1. Activa Nueva Arquitectura y valida en un dev client (o directamente en Expo Go SDK 55+).
  2. Actualiza @shopify/flash-list a la última v2.
  3. Ejecuta grep -R "estimatedItemSize\|estimatedListSize\|estimatedFirstItemOffset\|MasonryFlashList\|inverted" src y elimina/adapta cada aparición.
  4. Sustituye <MasonryFlashList /> por <FlashList masonry numColumns={2} />.
  5. Si tenías inverted, invierte tus datos con data={[...items].reverse()} o usa onStartReached para chats.
  6. Reemplaza el estado local por celda por useRecyclingState.

Uso correcto de useRecyclingState

import { FlashList, useRecyclingState } from '@shopify/flash-list';

function Row({ item }) {
  // Este estado se resetea automáticamente cuando la celda se recicla,
  // así el checkbox no "hereda" el valor de la fila anterior.
  const [checked, setChecked] = useRecyclingState(false, [item.id]);

  return (
    <Pressable onPress={() => setChecked((v) => !v)}>
      <Text>{item.title} {checked ? '☑' : '☐'}</Text>
    </Pressable>
  );
}

El segundo argumento ([item.id]) es la clave de reset: cuando el recycler asigna la celda a otro item, el hook detecta que la clave cambió y vuelve al valor inicial. Sin este hook, un useState normal mantiene el valor del item anterior y produce el bug clásico del "checkbox fantasma". Me tocó justamente ese bug en producción antes de que existiera el hook, y la solución con refs era fea.

Benchmarks 2026: qué medir y con qué herramienta

Los números publicados por Shopify (hasta 50% menos área en blanco) son un buen titular, pero para tomar decisiones sobre tu app necesitas medir tu propio caso. Mi flujo de perfilado en 2026 combina tres herramientas, y descarté Flipper por completo desde que dejó de mantenerse oficialmente para RN 0.73+:

  • Perfetto para Android: trazas system-wide con desglose por hilo (JS, UI, Render). Es el sucesor natural de systrace y captura eventos de Fabric.
  • Xcode Instruments (Time Profiler + Hitches) para iOS: mide "hitches" (frames largos) con precisión y muestra la pila JS gracias a los símbolos que Hermes expone.
  • React Native DevTools (Chrome DevTools frontend, integrado en RN 0.76+): profiler de JS/React y timeline unificado, sustituye al viejo React Native Debugger.

Un ciclo de medición útil dura menos de 15 minutos por pantalla:

  1. Compila en release con Hermes habilitado. Los benchmarks en debug son ruido.
  2. Fija el dispositivo a modo avión + wifi controlado para reducir varianza de red.
  3. Ejecuta un fling scroll estandarizado (mismo gesto, misma distancia, 4 segundos).
  4. Repite 10 veces y toma la mediana, no el promedio: así evitas que un pico de GC ensucie la comparación.
  5. Registra FPS medio, FPS mínimo, y % de tiempo del hilo JS por encima del 80%.

Para depurar cuellos concretos del hilo JS, la guía de testing y depuración con React Native DevTools cubre el flujo completo, incluyendo cómo exportar trazas para compartir con el equipo.

Patrones avanzados: masonry, useRecyclingState y sticky headers

Masonry (mosaico multi-columna)

<FlashList
  masonry
  numColumns={2}
  data={photos}
  keyExtractor={(p) => p.id}
  renderItem={({ item }) => (
    <Image
      source={{ uri: item.url }}
      style={{ aspectRatio: item.ratio, borderRadius: 8, margin: 4 }}
    />
  )}
/>

La prop masonry reemplaza al antiguo MasonryFlashList. Si mezclas alturas, deja que la aspectRatio haga el trabajo; el recycler distribuye las columnas balanceando altura acumulada.

Sticky headers agrupados

const flatData = useMemo(
  () => groups.flatMap((g) => [{ type: 'header', title: g.title }, ...g.items]),
  [groups]
);

<FlashList
  data={flatData}
  stickyHeaderIndices={flatData
    .map((it, i) => (it.type === 'header' ? i : null))
    .filter((i) => i !== null)}
  getItemType={(item) => (item.type === 'header' ? 'header' : 'row')}
  renderItem={({ item }) =>
    item.type === 'header' ? <SectionHeader title={item.title} /> : <Row item={item} />
  }
/>

getItemType es la joya poco conocida de FlashList: al distinguir tipos, el recycler mantiene pools separados y no intenta reutilizar un header como fila normal (lo que provocaría un re-render caro de la subárbol). Actívalo siempre que tengas al menos dos layouts distintos.

Animaciones dentro de la fila

Si combinas FlashList con Reanimated 4 y animaciones en React Native, mantén los useSharedValue dentro de la fila y usa useAnimatedStyle. Como Reanimated corre en el hilo UI, el reciclaje de FlashList no interfiere; simplemente reasigna la ref del componente animado.

Cuándo no usar FlashList

FlashList no es una solución universal. Hay tres escenarios donde FlatList, o incluso un simple ScrollView, sigue siendo la opción correcta:

  1. Listas muy cortas (<50 filas) con contenido estático. El coste de importar 40 KB de recycler no compensa cuando el ahorro de reciclaje es despreciable.
  2. Proyectos aún en arquitectura antigua. Si no puedes migrar por dependencias, quédate en FlashList v1.7 hasta que el ecosistema esté listo.
  3. Filas con altura fija idéntica y contenido trivial (texto plano). Con getItemLayout, FlatList es sorprendentemente competitivo; medí diferencia <3 fps.

Para el resto (feeds, catálogos, timelines, chats, dashboards con tarjetas), FlashList v2 debe ser tu default en 2026. La combinación de JS puro, layout de Fabric y recycler sin estimaciones cierra por fin el capítulo de "afinar props de FlatList al gusto". Revisa las notas de release de FlashList y la documentación oficial de FlashList para novedades entre minor versions.

Preguntas frecuentes

¿Es FlashList realmente más rápido que FlatList?

Sí, en listas con más de ~200 filas o filas medianamente complejas. En mis mediciones sobre un Pixel 5 con release y Hermes, FlashList v2 sostiene 59–60 fps donde FlatList cae a 40–50 fps. El recycler evita destruir y recrear vistas, que es el coste dominante durante un fling scroll.

¿Debo usar FlashList en 2026 aunque mi lista sea pequeña?

No necesariamente. Si tu lista tiene menos de 50 filas y filas simples, FlatList (o incluso ScrollView) es suficiente y evita una dependencia. Usa FlashList cuando midas dropped frames, tengas cientos o miles de items, o combines listas anidadas.

¿Funciona FlashList v2 con la Nueva Arquitectura?

Funciona exclusivamente con la Nueva Arquitectura. Requiere React Native 0.76+ (Fabric activo por defecto) y, en Expo, SDK 55 o superior con newArchEnabled: true. Si tu proyecto aún corre en la arquitectura antigua, pinea @shopify/flash-list@^1.7.

¿Cómo migro de FlashList v1 a v2 sin romper la app?

Primero activa la Nueva Arquitectura, luego actualiza el paquete y elimina las props estimatedItemSize, estimatedListSize, estimatedFirstItemOffset e inverted. Reemplaza MasonryFlashList por la prop masonry. Cambia estados locales por celda a useRecyclingState para evitar el bug del checkbox fantasma.

¿Cuáles son las desventajas de FlashList?

Requiere Nueva Arquitectura (imposible en apps antiguas sin migrar antes), añade una dependencia externa y su reciclaje puede sorprender si no memoizas correctamente renderItem o si usas useState en lugar de useRecyclingState. Para listas triviales, el beneficio no compensa el coste cognitivo.

¿Qué es LegendList y compite con FlashList?

LegendList es una librería reciente que también apunta a listas de altísimo rendimiento y mantiene 60fps con 10 000 items en escenarios donde FlashList ocasionalmente pierde frames. Es una alternativa a considerar, pero FlashList tiene un ecosistema más maduro y respaldo de Shopify en producción a gran escala.

Carlos Mendoza
Sobre el Autor Carlos Mendoza

Mobile performance engineer who profiles for a living. Has spent more hours in Flipper than he'll admit.