راهنمای کامل FlashList v2 در React Native: مهاجرت از FlatList و بهینه‌سازی لیست‌ها (۲۰۲۶)

راهنمای عملی مهاجرت از FlatList به FlashList v2 در React Native: نصب در Expo، تنظیم getItemType، رفع سلول‌های خالی، Masonry و سازگاری با معماری جدید (Fabric + JSI).

FlashList v2 راهنمای مهاجرت (۲۰۲۶)

به‌روزرسانی: ۲ اوت ۲۰۲۶

FlashList v2 نسخهٔ بازنویسی‌شدهٔ کامپوننت لیست Shopify برای React Native است که در اواخر ۲۰۲۵ روی معماری جدید (Fabric + JSI) از پایه ساخته شد و در پروژه‌های واقعی بین ۵ تا ۱۰ برابر سریع‌تر از FlatList عمل می‌کند و تا ۵۰٪ کاهش «سلول خالی» هنگام اسکرول سریع نسبت به v1 دارد. اگر شما هم مثل من از دنیای React Web به موبایل آمده‌اید، FlashList را می‌توان معادل مفهومی TanStack Virtual یا react-window دانست. با این تفاوت که ابعاد را دیگر لازم نیست دستی تخمین بزنید، الگوریتم layout جدید همه‌چیز را خودش یاد می‌گیرد.

  • FlashList v2 در ۲۰۲۶ تنها روی معماری جدید React Native (Fabric + JSI) کار می‌کند و به React Native 0.76 یا بالاتر نیاز دارد.
  • پراپ estimatedItemSize در v2 حذف شده و اندازه‌گیری آیتم‌ها به‌طور خودکار انجام می‌شود.
  • مهاجرت از FlatList معمولاً فقط با تغییر import و اضافه کردن getItemType انجام می‌شود؛ در بیشتر پروژه‌ها زیر ۱۵ دقیقه زمان می‌برد.
  • سلول‌های خالی (blank cells) در FlashList با memoize کردن renderItem و ارائهٔ getItemType صحیح از بین می‌روند.
  • کامپوننت جداگانهٔ MasonryFlashList منسوخ شده و اکنون تنها یک prop روی خود FlashList است.
  • در سناریوی چت‌های معکوس، maintainVisibleContentPosition در v2 به‌طور پیش‌فرض فعال است.

FlashList v2 چیست و چه تفاوتی با FlatList دارد؟

FlashList یک کامپوننت لیست virtualized است که تیم Shopify آن را برای رفع محدودیت‌های FlatList ساخت. نسخهٔ اول در ۲۰۲۲ روی RecyclerListView بنا شده بود، ولی نسخهٔ v2 که در پایان ۲۰۲۵ منتشر شد از پایه بازنویسی شده و دقیقاً بر روی API های Fabric و JSI ساخته شده است. تفاوت بنیادی این است که FlatList برای هر آیتم یک instance جدید از View می‌سازد و پس از خروج از صفحه آن را unmount می‌کند، در حالی که FlashList همان view را «بازیافت» (recycle) می‌کند، دقیقاً همان الگویی که در RecyclerView اندروید یا UITableView در iOS می‌بینیم.

اگر پیش‌زمینه React Web دارید، این مفهوم شبیه TanStack Virtual یا react-window است: به‌جای رندر همهٔ ردیف‌ها، فقط پنجرهٔ قابل مشاهده رندر می‌شود. تفاوت اصلی این‌جاست که در وب معمولاً بر پایهٔ ارتفاع ثابت کار می‌کنیم و باید estimateSize بدهیم، اما در FlashList v2 دیگر لازم نیست هیچ تخمینی وارد کنید؛ اندازه‌گیری روی UI thread و در سمت Fabric انجام می‌شود.

ویژگیFlatListFlashList v2
مصرف حافظه در لیست ۱۰٬۰۰۰ آیتمبالا و رشد خطیثابت و پایین
سرعت اسکرول در اندروید متوسط۳۰–۴۵ FPSپایدار ۶۰ FPS
نیاز به estimatedItemSizeنداردخیر (در v2 حذف شد)
پشتیبانی از New Architectureبله، اما بدون بهینه‌سازیالزامی و بومی
گرید Masonry / ناهم‌ارتفاعمحدودکامل با پراپ masonry
maintainVisibleContentPositionدستیپیش‌فرض فعال
سازگاری با Reanimated 4ناقصکامل

چرا لیست‌های React Native کند می‌شوند؟

در تجربهٔ من از انتقال پروژه‌های React Web به React Native، بزرگ‌ترین شگفتی معمولاً همین‌جاست: کدی که در مرورگر روان اجرا می‌شود، روی گوشی به‌شدت افت FPS نشان می‌دهد. دلیل ساده است. مرورگر خودش virtualization پیش‌فرض ندارد اما compositor مرورگر بسیار توانمند است و به GPU متکی است. در موبایل، هر ردیفی که به سلسله‌مراتب view اضافه می‌شود، هم UI thread و هم لایهٔ shadow tree را درگیر می‌کند و در معماری قدیم حتی از bridge هم عبور می‌کرد.

مشکلات رایجی که باعث کندی FlatList می‌شوند شامل این‌ها هستند: rerender های اضافی به دلیل عدم memoize کردن renderItem، عبور inline object ها به عنوان style، نداشتن keyExtractor پایدار، و مصرف بالای حافظه با آیتم‌های سنگین مثل تصویر و ویدیو. FlashList با bucket بندی view type ها و بازیافت آن‌ها این هزینه‌ها را از بین می‌برد، اما فقط زمانی که drop-in ساده انجام ندهید و پراپ‌های اصلی را درست تنظیم کنید.

یک مطالعهٔ موردی معروف از Whitespectre گزارش داد که پس از مهاجرت یک لیست تودرتوی پیچیده از FlatList به FlashList، مصرف CPU روی JS thread از پایداری بالای ۹۰٪ به پایین ۱۰٪ کاهش یافت و کرش‌های Out-of-Memory آن صفحه به کلی از بین رفت. برای درک عمیق‌تر این‌که چرا JSI و Fabric عملکرد لیست‌ها را بهبود می‌دهند، مقالهٔ راهنمای مهاجرت به معماری جدید React Native را ببینید.

نصب و راه‌اندازی FlashList v2 در Expo

خب، بریم سر اصل مطلب. نصب FlashList v2 در پروژه‌های Expo و bare React Native ساده است، ولی چون این نسخه فقط روی New Architecture کار می‌کند، ابتدا باید مطمئن شوید که پروژه‌تان روی این معماری قرار دارد.

نصب در پروژهٔ Expo

npx expo install @shopify/flash-list

# در صورت داشتن native code، prebuild بگیرید
npx expo prebuild --clean

نصب در پروژهٔ bare React Native

yarn add @shopify/flash-list@^2.0.0
# iOS
cd ios && pod install && cd ..

سپس در app.json پروژهٔ Expo مطمئن شوید که New Architecture فعال است. از React Native 0.76 به بعد این حالت پیش‌فرض است اما بررسی صریح آن ضرر ندارد:

{
  "expo": {
    "newArchEnabled": true,
    "jsEngine": "hermes"
  }
}

چگونه از FlatList به FlashList v2 مهاجرت کنیم؟

راستش، مهاجرت در بیشتر موارد یک تغییر import ساده است. تصور کنید یک لیست FlatList برای نمایش پست‌ها دارید:

// قبل: با FlatList
import { FlatList, View, Text } from 'react-native';

export function PostsList({ posts }) {
  return (
    <FlatList
      data={posts}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => (
        <View style={{ padding: 16 }}>
          <Text>{item.title}</Text>
        </View>
      )}
    />
  );
}

نسخهٔ FlashList v2 آن به این شکل خواهد بود:

// بعد: با FlashList v2
import { FlashList } from '@shopify/flash-list';
import { View, Text, StyleSheet } from 'react-native';
import { useCallback, memo } from 'react';

const Row = memo(({ item }) => (
  <View style={styles.row}>
    <Text>{item.title}</Text>
  </View>
));

const styles = StyleSheet.create({ row: { padding: 16 } });

export function PostsList({ posts }) {
  const renderItem = useCallback(
    ({ item }) => <Row item={item} />,
    []
  );

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

توجه کنید که در v2 دیگر estimatedItemSize لازم نیست. اما اگر آیتم‌های شما چند نوع (type) مختلف دارند (مثلاً ترکیبی از هدر، متن، و تصویر)، باید getItemType را برای عملکرد بهینه فراهم کنید:

<FlashList
  data={feed}
  keyExtractor={(item) => item.id}
  getItemType={(item) => item.kind} // 'text' | 'image' | 'video'
  renderItem={({ item }) => {
    if (item.kind === 'image') return <ImageCard data={item} />;
    if (item.kind === 'video') return <VideoCard data={item} />;
    return <TextCard data={item} />;
  }}
/>

getItemType باعث می‌شود FlashList برای هر نوع، pool جداگانه‌ای از view های قابل بازیافت نگه دارد. این نکتهٔ کلیدی برای فیدهای ناهمگون مانند اینستاگرام یا توییتر است که ارتفاع سلول‌ها به‌شدت فرق دارد.

ویژگی‌های جدید FlashList v2

نسخهٔ v2 چند تغییر بنیادی نسبت به v1 دارد که کار توسعه‌دهنده را ساده‌تر می‌کند:

  • Auto-sizing کامل: ابعاد آیتم‌ها به‌طور خودکار روی UI thread اندازه‌گیری و کش می‌شوند. پراپ‌های estimatedItemSize، estimatedListSize و overrideItemLayout حذف شده‌اند.
  • Masonry داخلی: کامپوننت جداگانهٔ MasonryFlashList منسوخ شده و اکنون فقط پراپ masonry را روی خود FlashList ست می‌کنید.
  • هوک useRecyclingState: state داخل هر آیتم که هنگام بازیافت باید ری‌ست شود (مثل play/pause برای ویدیو).
  • maintainVisibleContentPosition پیش‌فرض: برای چت‌های معکوس مثل واتساپ دیگر لازم نیست از trick های سفارشی استفاده کنید.
  • onStartReached و onLoad: callback های جدید برای infinite scroll دو طرفه و اندازه‌گیری زمان اولین رندر که برای Core Web Vitals معادل موبایل کاربردی است.
  • پشتیبانی بهتر از Web: در React Native Web با یک shim کوچک اجرا می‌شود و به عنوان virtual list در مرورگر هم عمل می‌کند.

رفع سلول‌های خالی و پرش (jank)

سلول‌های خالی (blank cells) رایج‌ترین شکایت درباره FlashList است. این اتفاق زمانی می‌افتد که کاربر بسیار سریع اسکرول می‌کند و الگوریتم بازیافت به‌موقع نمی‌تواند آیتم را رندر کند. Shopify ادعا کرده که در v2 این ناحیهٔ سفید تا ۵۰٪ کاهش یافته است، اما در برخی سناریوها همچنان می‌بینید. صادقانه بگویم، من در پروژهٔ آخرم دقیقاً به همین باگ برخوردم و راه‌حل‌ها این‌ها بودند:

۱. نبود getItemType در فیدهای ناهمگون

وقتی هر آیتم ارتفاع بسیار متفاوتی دارد، بدون getItemType کش بازیافت بی‌فایده می‌شود چون FlashList نمی‌داند کدام view را برای کدام نوع دوباره استفاده کند. همیشه یک شناسه از نوع string یا number برگردانید.

۲. renderItem سنگین یا غیر memoized

// بد: هر رندر یک تابع جدید ساخته می‌شود
<FlashList renderItem={({ item }) => <Row item={item} />} />

// خوب: memoize با useCallback و کامپوننت با React.memo
const Row = React.memo(({ item }) => { /* ... */ });
const renderItem = useCallback(({ item }) => <Row item={item} />, []);

۳. تصاویر بدون کش

از expo-image استفاده کنید که کش داخلی on-disk دارد. تصاویر بدون کش هر بار پس از هر بازیافت شبکه را می‌زنند و باعث پرش می‌شوند. همچنین از placeholder کم‌کیفیت (BlurHash یا ThumbHash) استفاده کنید تا تجربه روان‌تر شود.

۴. تنظیم drawDistance

در v2 می‌توانید drawDistance را افزایش دهید تا FlashList زودتر آیتم‌های آتی را آماده کند:

<FlashList drawDistance={500} /* پیکسل */ />

۵. تست در حالت Release

هرگز عملکرد FlashList را در حالت dev قضاوت نکنید. در dev mode، React Native با warnings، render buffer کوچک‌تر و بدون Hermes bytecode بهینه اجرا می‌شود و FlashList به‌طور مصنوعی کندتر به نظر می‌رسد. حتماً روی build release یا حداقل با --variant release تست کنید.

سازگاری با معماری جدید React Native

FlashList v2 اولین لیستی است که کاملاً روی JSI و Fabric طراحی شده. یعنی به‌جای عبور از bridge برای هر layout event، مستقیماً با ShadowNode های C++ صحبت می‌کند. این کار در دستگاه‌های اندروید ضعیف تفاوت محسوسی ایجاد می‌کند. در مطالعه‌های موردی داخلی Shopify، زمان اولین کشیدن (Time to First Interactive) لیست بین ۲۰ تا ۴۰ درصد کاهش یافته است.

برای گرفتن حداکثر بهره، این چک‌لیست را دنبال کنید:

  • React Native 0.76 یا بالاتر (ترجیحاً 0.82 که bridge کاملاً حذف شده و 0.84 که Hermes V1 پیش‌فرض است).
  • Hermes به عنوان engine جاوااسکریپت (پیش‌فرض).
  • Reanimated 4 برای انیمیشن‌های ردیف‌ها و هدر کالاپس‌شونده.
  • expo-image یا react-native-fast-image برای رندر تصاویر با کش داخلی.
  • react-native-gesture-handler نسخهٔ ۲.۲۰ یا بالاتر برای swipe-to-delete بدون تداخل touch.

Masonry، چت معکوس و الگوهای پیشرفته

برای گریدهای Pinterest-style که ارتفاع کارت‌ها متفاوت است، در v2 دیگر لازم نیست کامپوننت جداگانه‌ای import کنید. کافی است پراپ masonry را روی خود FlashList ست کنید:

<FlashList
  data={photos}
  numColumns={2}
  masonry
  optimizeItemArrangement
  keyExtractor={(item) => item.id}
  renderItem={({ item }) => (
    <Image
      source={{ uri: item.url }}
      style={{ width: '100%', aspectRatio: item.aspectRatio }}
    />
  )}
/>

پرچم optimizeItemArrangement باعث می‌شود موتور layout آیتم‌ها را به‌گونه‌ای بچیند که ستون‌ها ارتفاع متوازن داشته باشند، دقیقاً مثل کاری که column-fill: balance در CSS Multi-column Layout انجام می‌دهد.

لیست‌های چت معکوس

<FlashList
  data={messages}
  inverted
  // در v2 این prop به‌طور پیش‌فرض فعال است، اما می‌توانید override کنید
  maintainVisibleContentPosition={{
    minIndexForVisible: 0,
    autoscrollToTopThreshold: 100,
  }}
  keyExtractor={(m) => m.id}
  renderItem={({ item }) => <MessageBubble msg={item} />}
/>

این تنظیم تضمین می‌کند وقتی پیام جدید در ابتدای لیست اضافه می‌شود، موقعیت اسکرول کاربر ثابت می‌ماند — دقیقاً همان تجربه‌ای که در واتساپ و تلگرام می‌بینید. اگر با Expo Router و ناوبری stack کار می‌کنید، مطالعهٔ راهنمای Deep Linking در Expo Router برای هدایت به یک پیام خاص از notification مفید خواهد بود.

هوک useRecyclingState

وقتی view بازیافت می‌شود، ممکن است state داخلی آن (مثلاً play state ویدیو یا expanded state یک کارت) باید ری‌ست شود. در گذشته این کار با useEffect پیچیده‌ای انجام می‌شد؛ در v2 هوک اختصاصی useRecyclingState اضافه شده:

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

function VideoCard({ item }) {
  // در هر بازیافت به مقدار اولیه بازمی‌گردد
  const [isPlaying, setIsPlaying] = useRecyclingState(false, [item.id]);
  return <VideoPlayer uri={item.url} playing={isPlaying} onToggle={setIsPlaying} />;
}

اشتباهات رایج و نکات عملکردی

در طول بررسی PR های تیم‌های مختلف، این الگوها را بارها دیده‌ام:

  1. قرار دادن FlashList درون ScrollView: این کار الگوریتم virtualization را می‌شکند و FlashList مجبور می‌شود همه چیز را همزمان رندر کند. اگر به هدرهای اسکرول‌شونده نیاز دارید، از ListHeaderComponent استفاده کنید.
  2. استفاده از index به عنوان key: این کار باعث ری‌رندر اضافه می‌شود، مخصوصاً هنگام drag-and-drop یا حذف. همیشه یک ID پایدار بدهید.
  3. عبور object literal به style: از StyleSheet.create یا style های ثابت خارج از رندر استفاده کنید تا reference پایدار بماند و React.memo کار خودش را انجام دهد.
  4. عدم استفاده از React.memo برای Row: بدون memo، هر بار که parent رندر می‌شود، همهٔ ردیف‌ها هم رندر می‌شوند و مزیت بازیافت از بین می‌رود.
  5. فراموش کردن onEndReachedThreshold: مقدار پیش‌فرض 0.5 است؛ برای فیدهای سریع مقدار 2 مناسب‌تر است تا صفحهٔ بعد زودتر fetch شود.
  6. تست فقط روی iOS Simulator یا دستگاه‌های سطح‌بالا: همیشه روی یک اندروید mid-range (مثل Pixel 4a یا Galaxy A32) تست کنید، این‌جاست که تفاوت FlashList نمایان می‌شود.

برای اندازه‌گیری واقعی عملکرد از راهنمای عملکرد رسمی React Native، React Native DevTools و flipper Perf plugin استفاده کنید. همچنین مخزن رسمی FlashList در گیت‌هاب و راهنمای مهاجرت v1 به v2 نمونه‌های benchmark ارائه می‌دهند که می‌توانید در دستگاه خود اجرا کنید.

پرسش‌های متداول

آیا FlashList v2 با پروژه‌های Expo Managed سازگار است؟

بله، از Expo SDK 52 به بعد به‌طور کامل پشتیبانی می‌شود. کافی است با npx expo install @shopify/flash-list نصب کنید و در صورت داشتن native code یا config plugin، یک npx expo prebuild --clean بگیرید. برای Expo Go تنها v1 پشتیبانی می‌شود، اما در development build از v2 استفاده می‌کنید.

آیا باید همهٔ FlatList های پروژه را به FlashList مهاجرت دهم؟

خیر؛ برای لیست‌های کوتاه (کمتر از ۳۰ تا ۵۰ آیتم) تفاوت محسوسی نیست و پیچیدگی اضافه کردن dependency ارزش ندارد. برای فیدها، جستجوها، چت‌ها و هر لیستی با بیش از ۱۰۰ آیتم مهاجرت را قویاً توصیه می‌کنم. Shopify هم رسماً پیشنهاد می‌دهد هر لیست بزرگ‌تر از یک صفحهٔ محتوا را روی FlashList بگذارید.

تفاوت FlashList با Legend List چیست؟

Legend List رقیب جدیدی از LegendApp است که در benchmarks اولیه CPU و RAM کمتری در لیست‌های ۵۰۰۰+ آیتمی نشان می‌دهد. اما FlashList v2 با پشتوانهٔ Shopify، جامعهٔ کاربری بسیار بزرگ‌تر و سازگاری بهتر با کتابخانه‌ها همچنان انتخاب پیش‌فرض و امن برای پروژه‌های production است. Legend List گزینه‌ای عالی برای تیم‌هایی است که از ابتدا روی Fabric ساخته می‌شوند و به بالاترین سطح عملکرد نیاز دارند.

چگونه سلول‌های خالی FlashList را دیباگ کنم؟

ابتدا مطمئن شوید در حالت release تست می‌کنید. سپس getItemType را اضافه کنید، renderItem را با useCallback و کامپوننت را با React.memo بپیچانید و در آخر drawDistance را افزایش دهید. اگر همچنان مشکل داشتید، از callback جدید onLoad برای اندازه‌گیری زمان اولین رندر و از React Native DevTools برای پروفایل کردن استفاده کنید.

آیا FlashList با SectionList جایگزین می‌شود؟

FlashList معادل مستقیم SectionList ندارد، اما می‌توانید داده‌ها را flat کنید (هدرها و ردیف‌ها در یک آرایه) و با getItemType نوع «header» و «row» را متمایز کنید. این الگو در بیشتر سناریوها کارایی به‌مراتب بهتری نسبت به SectionList دارد و کد ساده‌تر هم می‌شود.

Anita Iyer
درباره نویسنده Anita Iyer

Cross-platform mobile developer who came to RN from web. Bridges the two worlds and explains the seams.