بیست آیتم در یک فید عالی به نظر میرسد. اسکرول کردن مثل کره روان است. مشتری شما خوشحال است. سپس محصول را به مرحله تولید (production) میرسانید، دادهها از راه میرسند و ناگهان با دو هزار ردیف روبرو میشوید. رابط کاربری (UI) شروع به لگ زدن میکند. مصرف حافظه بالا میرود تا اینکه سیستمعامل برنامه را میبندد. در ناامیدی، برخی از توسعهدهندگان همه چیز را در یک ScrollView میپیچند و کار را تمام شده میپندارند. این تصمیم معمولاً به ازای هر باگی که حل میکند، سه باگ جدید ایجاد میکند.
لیستها گلوگاه عملکردی هستند که نحوه درک کاربران از اپلیکیشن React Native شما را تعیین میکنند. اگر آنها را درست پیادهسازی کنید، اپلیکیشن حس Native بودن میدهد. اگر اشتباه انجام دهید، حتی زیباترین صفحه هم به یک تجربه کند و طاقتفرسا تبدیل میشود. علت اصلی معمولاً عدم تطابق بین کامپوننت انتخابی شما و کاری است که از تردهای JavaScript و UI میخواهید انجام دهند. React Native روی دو مسیر اجرا میشود. منطق برنامه شما در JS thread قرار دارد، در حالی که عملیات ترسیم (painting) در native UI thread انجام میشود. وقتی یک لیست عظیم را به اشتباه رندر میکنید، هر دو ترد در محاسبات چیدمان (layout)، رندر مجدد (re-renders) و تخصیص حافظه (memory allocations) غرق میشوند. نتیجه آن افت فریم (dropped frames)، پرشهای سفید و در نهایت کرش کردن است.
انتخاب ابزار مناسب
انتخاب یک کامپوننت لیست باید یک تصمیم معماری آگاهانه باشد، نه یک واکنش غریزی.
ScrollView سادهترین گزینه است. هر فرزند (child) که به آن بدهید را میگیرد، تکتک آنها را بلافاصله در حافظه بارگذاری (mount) میکند و کل آنها را به موتور اسکرول native تحویل میدهد. این دقیقاً همان چیزی است که برای محتوای کوتاه و ثابت مانند صفحه تنظیمات، فرم ورود یا یک صفحه جزئیات محصول استاتیک با ده بخش نیاز دارید. این گزینه قابل پیشبینی است و استایلدهی به آن آسان است. نکته اینجاست که هیچ مجازیسازی (virtualization) در کار نیست. اگر دو هزار آیتم به آن بدهید، مطیعانه دو هزار native view ایجاد میکند. از ScrollView برای مجموعهدادههای بزرگ یا پویا استفاده نکنید. آن را به عنوان یک پوستر قابشده تصور کنید، نه قفسه کتابخانه.
FlatList اسب بارکش برای فیدهای طولانی و یکنواخت است. این کامپوننت محتوا را مجازیسازی میکند، به این معنی که فقط ردیفهایی را بارگذاری (mount) میکند که در حال حاضر قابل مشاهده هستند یا نزدیک به محدوده دید (viewport) قرار دارند. با اسکرول کردن کاربر، FlatList سلولهایی را که از صفحه خارج میشوند از حالت بارگذاری خارج (unmount) کرده و آنها را برای دادههای جدید بازیافت (recycle) میکند. این کار باعث میشود میزان مصرف حافظه، صرفنظر از اینکه آرایه شما چقدر بزرگ شود، ثابت بماند. اگر در حال ساخت یک تایملاین اجتماعی، مرکز اعلانها یا هر مجموعه از کارتهای مشابه هستید که به طور مداوم اسکرول میشوند، FlatList انتخاب پیشفرض و درست است.
SectionList در واقع همان FlatList با قابلیت سازماندهی است. زمانی از آن استفاده کنید که دادههای شما در دستههای گروهبندی شده میرسند؛ مانند دفترچه تلفن مرتب شده بر اساس حروف الفبا، گزارش تمرینات تقسیم شده بر اساس تاریخ، یا لیست فاکتورها که بر اساس ماه سازماندهی شدهاند. این کامپوننت هدرهای بخش (sticky section headers) را رندر میکند و منطق گروهبندی را برای شما مدیریت میکند. در لایههای زیرین، از همان موتور مجازیسازی FlatList استفاده میکند، بنابراین همان مزایای حافظه را با ساختار اضافیِ بخشهای دارای عنوان دریافت میکنید.
FlashList زمانی که نیاز دارید آخرین فریمهای ممکن را از دستگاه بیرون بکشید، وارد عمل میشود. این ابزار که بر پایه اکوسیستم RecyclerListView ساخته شده است، ویوها را تهاجمیتر از FlatList بازیافت میکند و هدف آن حفظ ۶۰ فریم در ثانیه حتی در سختافزارهای میانرده است. اگر در حال ساخت یک رابط کاربری چت با حجم بالا، کاتالوگ محصول با سرعت اسکرول زیاد، یا هر صفحهای هستید که در آن روانی حرکت (smoothness) یک مزیت رقابتی محسuh میشود، FlashList ارزش اضافه کردن این وابستگی (dependency) را دارد. این ابزار برای هر صفحهای ضروری نیست، اما برای فیدهایی که تجربه اصلی کاربر را تعریف میکنند، تفاوت عملکرد (performance delta) کاملاً محسوس است.
قاتلان رایج عملکرد
وقتی یک لیست شروع به کند شدن میکند، سه متهم همیشگی وجود دارند.
بارگذاری (Mounting) همزمان درختهای React بسیار زیاد، دراماتیکترین نوع شکست است. وقتی هر ردیف یک درخت کامپوننت پیچیده باشد، رندر اولیه میتواند JS thread را آنقدر طولانی مسدود کند که یک صفحه سفید خالی یا یک اولین ترسیم (first paint) بسیار کند و زشت ایجاد شود. کاربر اپلیکیشن را باز میکند و منتظر میماند. حتی پس از بارگذاری اولیه، ردیفهای سنگین باعث کند شدن شروع اسکرول میشوند، زیرا چند فریم اول صرف کارهای آمادهسازی (setup) میشود.
انجام کار بیش از حد در هر فریم، خود را به صورت لرزش (stuttering) در حین اسکرول نشان میدهد. شما برای حفظ روانی انیمیشنها، بودجهای در حدود شانزده میلیثانیه برای هر فریم دارید. اگر یک کامپوننت ردیف، محاسبات سنگین انجام دهد، تاریخها را در لحظه تجزیه (parse) کند، یا مقایسههای عمیق اشیاء (deep object comparisons) را داخل رندر انجام دهد، این بودجه را از دست میدهید. ترد UI فریمها را از دست میدهد و کاربر تکان ناگهانی (jolt) را حس میکند.
استفاده بیش از حد از حافظه، قاتل خاموش است. هر native view هزینهای از RAM تحمیل میکند. اگر تصاویر بزرگ و بهینهنشده، سایههای Drop Shadow روی هر کارت، یا touchableهای تو در تو اضافه کنید، میزان مصرف حافظه (footprint) چندین برابر میشود. در iOS ممکن است سیستم بدون هشدار برنامه شما را ببندد. در Android، کاربر شاهد انباشته شدن لگها خواهد بود تا زمانی که اپلیکیشن غیرقابل استفاده شود.
چکلیست بهینهسازی
عادتهای استراتژیک کوچک، تفاوتی است میان لیستی که صرفاً کار میکند و لیستی که با سرعت و روانی بینظیر اجرا میشود.
از کلیدهای پایدار استفاده کنید. همیشه یک شناسه واقعی از مجموعه دادههای خود را به پراپ key پاس دهید. هرگز از ایندکس آرایه استفاده نکنید. اگر لیست شما تغییر ترتیب دهد، فیلتر شود یا آیتمی به آن اضافه شود، یک کلید مبتنی بر ایندکس، React را فریب میدهد تا دادههای اشتباه را با کامپوننت بازیافتشدهی اشتباه جفت کند. این اشتباه باعث ایجاد unmountهای غیرضروری، عدم تطابق وضعیت (state mismatch) و رندرهای مجدد زنجیرهای میشود. یک ID مناسب دقیقاً به React میگوید که کدام ردیف به کجا منتقل شده است.
ردیفها را memoize کنید. کامپوننت ردیف خود را در React.memo قرار دهید تا تنها زمانی که پراپهای آن واقعاً تغییر میکنند، دوباره رندر شود. بدون این محافظ، هر بهروزرسانی در وضعیت (state) والد میتواند باعث اجرای یک چرخه رندر برای تمام ردیفهای قابل مشاهده شود، حتی اگر دادههای آنها یکسان باشد. در یک لیست طولانی که مدام در حال تغییر است، این چرخههای هدر رفته به سرعت روی هم انباشته میشوند.
renderItem را پایدار نگه دارید. از تعریف یک تابع جدید مستقیماً داخل پراپ renderItem در هر بار رندر شدن والد خودداری کنید. یک تابع arrow function درونخطی مانند renderItem={({ item }) => <Row data={item} />} هر بار که والد بهروزرسانی میشود، یک مرجع (reference) جدید ایجاد میکند. FlatList یک پراپ تغییریافته را مشاهده کرده و ردیف را به طور غیرضروری بازیافت میکند. تابع رندر را خارج از کامپوننت تعریف کنید یا آن را با useCallback memoize کنید تا مرجع آن پایدار بماند.
هر زمان که امکان دارد از getItemLayout استفاده کنید. اگر ردیفهای شما ارتفاع ثابت یا قابل پیشبینی دارند، دقیقاً آن را به FlatList بگویید. این پراپ به لیست اجازه میدهد تا از فراخوانیهای هزینهبر اندازهگیری native صرفنظر کند. لیست به جای اندازهگیری هر سلول پس از mount شدن، موقعیت را به صورت ریاضی محاسبه میکند. این تفاوت بهویژه در لیستهایی با صدها یا هزاران آیتم بسیار چشمگیر است، جایی که رفتوآمد مداوم (chatter) مربوط به onLayout میتواند باعث از کار افتادن JS thread شود.
تصاویر را به شدت بهینه کنید. تصاویر بدون ابعاد مشخص، سمِ لیست هستند. همیشه عرض و ارتفاع صریح تعیین کنید تا لایه native قبل از رمزگشایی (decode) تصویر، فضا را رزرو کند. برای تصاویر از راه دور (remote)، از یک کتابخانه کشینگ مانند Expo Image یا معادل آن استفاده کنید که کشینگ حافظه، ماندگاری روی دیسک و بهینهسازی فرمت را مدیریت کند. کامپوننت پیشفرض React Native Image برای نمونههای اولیه (prototypes) مناسب است، اما فیدهای محیط عملیاتی (production) به کنترل بیشتری روی حافظه و وضعیتهای بارگذاری نیاز دارند.
از قرار دادن کانتینرهای اسکرول در هم خودداری کنید. هرگز یک FlatList عمودی را داخل یک ScrollView عمودی قرار ندهید. ScrollView والد تمام رویدادهای اسکرول را دریافت کرده و توانایی FlatList فرزند برای اندازهگیری viewport خود را مختل میکند. مجازیسازی (virtualization) از کار میافتد زیرا FlatList دیگر نمیداند کدام ردیفها باید قابل مشاهده باشند. نتیجه این است که هر ردیف در هر صورت mount میشود و هدف اصلی مجازیسازی از بین میرود. اگر به یک هدر بالای لیست نیاز دارید، از پراپ ListHeaderComponent خودِ FlatList استفاده کنید. اگر به رفتار چسبنده (sticky) پیچیده نیاز دارید، از یک SectionList یا FlashList با پیکربندی مناسب برای هدر استفاده کنید.
قانون طلایی
اگر محتوا کم و محدود است، اجازه دهید ScrollView آن را مدیریت کند. اگر محتوا با دادههای تولید شده توسط کاربر یا صفحهبندی از راه دور (remote pagination) رشد میکند، از یک لیست مجازی (virtualized list) استفاده کنید. زمانی که لیست هسته اصلی اپلیکیشن است و کاربران برای دقایق متمادی اسکرول میکنند، سراغ FlashList بروید.
یک حقیقت دیگر وجود دارد که فراموش کردنش آسان است: ردیفهای ساده، سریع اسکرول میشوند. هرچه کامپوننت ردیف شما سبکتر باشد، لیست شما روانتر خواهد بود. ناوبریهای (navigations) تودرتو، محاسبات سنگین و انیمیشنهای بیمورد را از هر ردیف جدا کنید. ساختار (markup) را تخت، منطق را سبک و تصاویر را با اندازه مشخص نگه دارید. بقای یک لیست به وزن تجمعی آنچه رندر میکند بستگی دارد. هر ردیف را سبک نگه دارید تا لیست در بهترین حالت ممکن، بسیار باکیفیت و روان به نظر برسد.
