עשרים פריטים בפיד מרגישים מושלמים. הגלילה חלקה כמו חמאה. הלקוח שלכם מרוצה. ואז אתם מעלים לסביבת הייצור (production), הנתונים מגיעים, ופתאום אתם בוהים בשני אלף שורות. ממשק המשתמש (UI) מתחיל להיתקע. צריכת הזיכרון עולה עד שהמערכת (OS) מפסיקה את הפעולה. מתוך ייאוש, חלק מהמפתחים עוטפים הכל ב-ScrollView וממשיכים ביום העבודה. ההחלטה הזו בדרך כלל מולידה שלושה באגים חדשים על כל אחד שהיא פותרת.
רשימות הן צוואר הבקבוק של הביצועים שקובע איך המשתמשים תופסים את אפליקציית ה-React Native שלכם. תעשו אותן נכון, והאפליקציה תרגיש native. תעשו אותן לא נכון, וגם המסך היפה ביותר יהפוך למעיק. שורש הבעיה הוא בדרך כלל חוסר התאמה בין הקומפוננטה שבחרתם לבין העבודה שאתם מבקשים מ-JavaScript ו-UI threads לבצע. React Native רצה על שני מסלולים. הלוגיקה שלכם חיה ב-JS thread בעוד שהציור (painting) מתבצע ב-native UI thread. כשאתם מרנדרים רשימה ענקית בצורה לא נכונה, שני ה-threads מתחילים לטבוע בחישובי פריסה (layout calculations), רינדורים מחדש (re-renders) והקצאות זיכרון. התוצאה היא נפילת פריימים (dropped frames), הבזקים לבנים, ובסופו של דבר קריסה.
בחרו את הכלי הנכון
בחירת קומפוננטת רשימה צריכה להיות החלטה ארכיטקטונית מחושבת, לא תגובת רפלקס.
ScrollView היא האופציה הפשוטה ביותר. היא לוקחת כל ילד (child) שאתם נותנים לה, מעלה (mounts) כל אחד מהם לזיכרון באופן מיידי, ומעבירה את כל הערימה למנוע הגלילה ה-native. זה בדיוק מה שאתם רוצים עבור תוכן קצר וקבוע, כמו מסך הגדרות, טופס התחברות, או דף פרטי מוצר סטטי עם עשרה סעיפים. היא צפויה וקלה לעיצוב. החיסרון הוא שאין וירטואליזציה (virtualization). אם תזינו לה אלפיים פריטים, היא תיצור בציות אלפיים native views. אל תשתמשו ב-ScrollView עבור סטים של נתונים גדולים או דינמיים. חשבו עליה כעל פוסטר במסגרת, לא כעל מדף ספרייה.
FlatList היא סוס העבודה עבור פידים ארוכים ואחידים. היא מבצעת וירטואליזציה של התוכן, מה שאומר שהיא מעלה (mounts) רק שורות שנמצאות כרגע גלויות או קרובות לפורט (viewport). בזמן שהמשתמש גולל, FlatList מפרקת (unmounts) תאים שעוזבים את המסך וממחזרת אותם עבור נתונים נכנסים. זה שומר על צריכת זיכרון יציבה ללא קשר לגודל המערך שלכם. אם אתם בונים ציר זמן של רשת חברתית, מרכז התראות, או כל אוסף של כרטיסים דומים שגוללים ברציפות, FlatList היא הבחירה הנכונה כברירת מחדל.
SectionList היא FlatList עם תחושה של ארגון. השתמשו בה כאשר הנתונים שלכם מגיעים בקבוצות, כמו ספר כתובות ממוין אלפביתית, יומן אימונים המחולק לפי תאריך, או רשימת חשבוניות מאורגנת לפי חודש. היא מרנדרת כותרות סקשן דביקות (sticky section headers) ומטפלת בלוגיקת הקיבוץ עבורכם. מתחת למכסה המנוע היא משתמשת באותו מנוע וירטואליזציה כמו FlatList, כך שאתם מקבלים את אותם יתרונות זיכרון עם המבנה הנוסף של חלוקה לכותרות.
FlashList נכנסת לתמונה כשאתם צריכים להפיק כל פריים אחרון מהמכשיר. בנויה על גבי האקוסיסטם של RecyclerListView, היא ממחזרת views בצורה אגרסיבית יותר מ-FlatList ושואפת לשמור על שישים פריימים לשנייה אפילו בחומרה בינונית. אם אתם בונים ממשק צ'אט בעל נפח גבוה, קטלוג מוצרים עם מהירות גלילה גבוהה, או כל מסך שבו חלקלקות היא יתרון תחרותי, FlashList שווה את התלות הנוספת. היא לא נחוצה לכל מסך, אבל עבור פידים שמגדירים את חווית הליבה, ההבדל בביצועים מורגש.
רוצחי ביצועים נפוצים
ישנם שלושה חשודים משותפים כשרשימה מתחילה לגרור.
העלאה (Mounting) של יותר מדי עצי React בבת אחת היא הכישלון הדרמטי ביותר. כשכל שורה היא עץ קומפוננטות מורכב, הרינדור הראשוני יכול לחסום את ה-JS thread מספיק זמן כדי ליצור מסך לבן ריק או "צבע ראשוני" (first paint) מאוחר ומכוער. המשתמש פותח את האפליקציה ומחכה. אפילו אחרי הטעינה הראשונית, שורות כבדות הופכות את אתחול הגלילה לאיטי מכיוון שהפריימים הראשונים נצרכים לעבודת ההגדרה (setup).
יותר מדי עבודה לכל פריים מתבטאת ברעידות בזמן הגלילה. יש לכם תקציב של בערך שש עשרה מילישניות לכל פריים כדי לשמור על אנימציות חלקות. אם קומפוננטת שורה מריצה חישובים יקרים, מנתחת תאריכים תוך כדי תנועה, או מבצעת השוואות אובייקטים עמוקות בתוך ה-render, אתם חורגים מהתקציב הזה. ה-UI thread מפיל פריימים והמשתמש מרגיש קפיצה.
שימוש מופרז בזיכרון הוא הרוצח השקט. כל native view עולה RAM. הוסיפו תמונות גדולות ולא אופטימליות, צללים (drop shadows) על כל כרטיס, או touchables מקוננים, והעקבות (footprint) יתרבים. ב-iOS המערכת עשויה לסגור את האפליקציה שלכם ללא אזהרה. ב-Android המשתמש צופה בעיכוב נבנה עד שהאפליקציה מרגישה בלתי ניתנת לשימוש.
צ'קליסט לאופטימיזציה
הרגלים אסטרטגיים קטנים הם אלו שמבדילים בין רשימה שסתם עובדת לבין כזו שמרחפת.
השתמשו במפתחות יציבים. תמיד העבירו מזהה אמיתי מתוך סט הנתונים שלכם ל-key prop. לעולם אל תשתמשו באינדקס המערך. אם הרשימה שלכם משנה סדר, מסננת או מוסיפה פריטים, מפתח מבוסס אינדקס מטעה את React וגורם לה לצמד נתונים לא נכונים לרכיב ממוחזר לא נכון. הטעות הזו גורמת ל-unmounts מיותרים, חוסר התאמה ב-state ו-re-renders שרשרתיים. ID תקין אומר ל-React בדיוק איזו שורה עברה לאן.
בצעו memoization לשורות. עטפו את רכיב השורה שלכם ב-React.memo כך שהוא יבצע re-render רק כאשר ה-props שלו באמת משתנים. ללא הגנה זו, כל עדכון state של רכיב אב עלול להפעיל סבב render בכל שורה גלויה, גם אם הנתונים שלהן זהים. ברשימה ארוכה ודינמית, מחזורי העיבוד המבוזבזים הללו מצטברים במהירות.
שמרו על renderItem יציב. הימנעו מהגדרת פונקציה חדשה ישירות בתוך ה-renderItem prop בכל פעם שהרכיב האב מבצע render. פונקציית חץ inline כמו renderItem={({ item }) => <Row data={item} />} יוצרת רפרנס חדש בכל פעם שהאב מתעדכן. ה-FlatList מזהה prop שונה וממחזר את השורה שלא לצורך. הגדירו את פונקציית ה-render מחוץ לרכיב או בצעו לה memoization באמצעות useCallback כדי שהרפרנס יישאר יציב.
השתמשו ב-getItemLayout בכל פעם שאפשר. אם לשורות שלכם יש גובה קבוע או צפוי, אמרו ל-FlatList בדיוק מהו הגובה. ה-prop הזה מאפשר לרשימה לדלג על קריאות מדידה native יקרות. במקום למדוד כל תא לאחר ה-mount, הרשימה מחשבת את המיקום באופן מתמטי. ההבדל בולט במיוחד ברשימות עם מאות או אלפי פריטים, שבהן "רעש" של onLayout עלול להכניע את ה-JS thread.
בצעו אופטימיזציה אגרסיבית לתמונות. תמונות ללא גודל מוגדר הן רעל לרשימות. תמיד הגדירו width ו-height מפורשים כדי שהשכבה ה-native תשמור מקום לפני שהתמונה עוברת decoding. עבור תמונות מרחוק, השתמשו בספריית caching כמו Expo Image או תחליף שמטפל ב-memory caching, שמירה בדיסק (disk persistence) ואופטימיזציה של פורמטים. רכיב ה-Image המובנה של React Native מתאים לאבות-טיפוס, אך ביישומי production נדרשת שליטה רבה יותר על הזיכרון ומצבי הטעינה.
הימנעו מקינון של קונטיינרים של גלילה. לעולם אל תשימו FlatList אנכי בתוך ScrollView אנכי. ה-ScrollView האב קולט את כל אירועי הגלילה ומשבש את היכולת של ה-FlatList הילד למדוד את ה-viewport שלו. ה-Virtualization נשבר כי ה-FlatList כבר לא יודע אילו שורות אמורות להיות גלויות. התוצאה היא שכל שורה מבצעת mount בכל מקרה, מה שמבטל את כל המטרה של ה-virtualization. אם אתם צריכים header מעל רשימה, השתמשו ב-ListHeaderComponent prop של ה-FlatList עצמו. אם אתם זקוקים להתנהגות sticky מורכבת, השתמשו ב-SectionList או ב-FlashList עם הגדרת ה-header המתאימה.
הכלל המוזהב
אם התוכן קטן ומוגבל, תנו ל-ScrollView לטפל בו. אם התוכן גדל עם נתונים שנוצרו על ידי משתמשים או באמצעות pagination מרחוק, השתמשו ברשימה מבוססת virtualization. כאשר הרשימה היא מרכז האפליקציה והמשתמשים יגללו במשך דקות בכל פעם, עברו ל-FlashList.
הנה אמת אחרונה שקל לשכוח: שורות משעממות גוללות מהר. ככל שרכיב השורה שלכם יהיה קל יותר, כך הרשימה תהיה חלקה יותר. הסירו ניווט מקונן (nested navigations), חישובים כבדים ואנימציות מיותרות מכל שורה בודדת. שמרו על markup שטוח, לוגיקה רזה ותמונות בגודל מוגדר. רשימה חיה או מתה לפי המשקל המצטבר של מה שהיא מרנדרת. הפכו כל שורה ל"זולה", והרשימה תרגיש יוקרתית בדרך הטובה ביותר.
