טבלאות ורשימות גדולות מאוד יכולות להאט את ביצועי האתר באופן משמעותי. הווירטואליזציה יכולה לעזור!
react-window היא ספרייה שמאפשרת הצגה יעילה של רשימות גדולות.
למה זה שימושי?
יכול להיות שתצטרכו להציג טבלה גדולה או רשימה עם הרבה שורות. טעינה של כל פריט ברשימה כזו עלולה להשפיע באופן משמעותי על הביצועים.
וירטואליזציה של רשימות, או 'חלונות', היא קונספט שבו רק מה שגלוי למשתמש מוצג. מספר האלמנטים שמוצגים בהתחלה הוא קבוצת משנה קטנה מאוד של הרשימה כולה, וה'חלון' של התוכן הגלוי נע כשהמשתמש ממשיך לגלול. כך משפרים את הביצועים של העיבוד והגלילה ברשימה.
צמתי DOM שיוצאים מה'חלון' ממוחזרים או מוחלפים מיד ברכיבים חדשים יותר כשהמשתמש גולל למטה ברשימה. כך מספר כל האלמנטים שעברו רינדור יהיה ספציפי לגודל החלון.
react-window
react-window היא ספרייה קטנה של צד שלישי שמקלה על יצירת רשימות וירטואליות באפליקציה. הוא מספק מספר ממשקי API בסיסיים שאפשר להשתמש בהם עבור סוגים שונים של רשימות וטבלאות.
מתי כדאי להשתמש ברשימות בגודל קבוע
אם יש לכם רשימה ארוכה וחד-ממדית של פריטים בגודל זהה, השתמשו ברכיב FixedSizeList.
import React from 'react';
import { FixedSizeList } from 'react-window';
const items = [...] // some list of items
const Row = ({ index, style }) => (
<div style={style}>
{/* define the row component using items[index] */}
</div>
);
const ListComponent = () => (
<FixedSizeList
height={500}
width={500}
itemSize={120}
itemCount={items.length}
>
{Row}
</FixedSizeList>
);
export default ListComponent;
- רכיב
FixedSizeListמקבל את המאפייניםheight,widthו-itemSizeכדי לשלוט בגודל הפריטים ברשימה. - פונקציה שמציגה את השורות מועברת כצאצא ל-
FixedSizeList. אפשר לגשת לפרטים על פריט מסוים באמצעות הארגומנטindex(items[index]). - פרמטר
styleמועבר גם לשיטת הרינדור של השורה, שחייבת להיות מצורפת לאלמנט השורה. הפריטים ברשימה ממוקמים באופן אבסולוטי עם ערכי הגובה והרוחב שמוקצים להם בשורה, והפרמטרstyleאחראי לכך.
מתי כדאי להשתמש ברשימות בגודל משתנה
משתמשים ברכיב VariableSizeList כדי להציג רשימה של פריטים בגדלים שונים. הרכיב הזה פועל כמו רשימה בגודל קבוע, אבל במקום ערך ספציפי, הוא מצפה לפונקציה במאפיין itemSize.
import React from 'react';
import { VariableSizeList } from 'react-window';
const items = [...] // some list of items
const Row = ({ index, style }) => (
<div style={style}>
{/* define the row component using items[index] */}
</div>
);
const getItemSize = index => {
// return a size for items[index]
}
const ListComponent = () => (
<VariableSizeList
height={500}
width={500}
itemCount={items.length}
itemSize={getItemSize}
>
{Row}
</VariableSizeList>
);
export default ListComponent;
הפונקציה item size שמועברת אל המאפיין itemSize מגרילה את גובה השורות בדוגמה הזו. אבל באפליקציה אמיתית, צריך להיות היגיון ממשי שמגדיר את הגדלים של כל פריט. מומלץ לחשב את הגדלים האלה על סמך נתונים או לקבל אותם מ-API.
רשתות
react-window תומך גם בהפיכת רשימות או רשתות רב-ממדיות לווירטואליות. בהקשר הזה, 'החלון' של התוכן הגלוי משתנה כשהמשתמש גולל אופקית וגם אנכית.
באופן דומה, אפשר להשתמש ברכיבים FixedSizeGrid ו-VariableSizeGrid בהתאם לאפשרות שהגודל של פריטים ספציפיים ברשימה ישתנה.
- ב-
FixedSizeGrid, ה-API כמעט זהה, אבל צריך לייצג את הגבהים, הרוחבים ומספרי הפריטים גם בעמודות וגם בשורות. - ב-
VariableSizeGrid, אפשר לשנות את רוחב העמודות ואת גובה השורות באמצעות העברת פונקציות במקום ערכים למאפיינים המתאימים.
אפשר לעיין בתיעוד כדי לראות דוגמאות לרשתות וירטואליות.
טעינה מדורגת בזמן גלילה
באתרים רבים משפרים את הביצועים על ידי המתנה לטעינה ולעיבוד של פריטים ברשימה ארוכה עד שהמשתמש גולל למטה. הטכניקה הזו, שנקראת בדרך כלל 'טעינה אינסופית', מוסיפה צמתי DOM חדשים לרשימה כשהמשתמש גולל מעבר לסף מסוים שקרוב לסוף. אמנם זה עדיף על טעינה של כל הפריטים ברשימה בבת אחת, אבל עדיין בסופו של דבר יאוכלס ה-DOM באלפי רשומות שורות אם המשתמש גלל מעבר למספר הזה. הדבר עלול להוביל לגודל DOM גדול מדי, שמתחיל להשפיע על הביצועים כי הוא מאט את חישובי הסגנונות ואת שינויי ה-DOM.
התרשים הבא יכול לעזור לכם להבין את זה:
הדרך הכי טובה לפתור את הבעיה הזו היא להמשיך להשתמש בספרייה כמו react-window כדי לשמור על "חלון" קטן של רכיבים בדף, אבל גם להשתמש בטעינה עצלה של רשומות חדשות כשהמשתמש גולל למטה. חבילה נפרדת, react-window-infinite-loader, מאפשרת זאת עם react-window.
בדוגמה הבאה מוצג קטע קוד שמראה מצב שמנוהל ברכיב App אב.
import React, { Component } from 'react';
import ListComponent from './ListComponent';
class App extends Component {
constructor(props) {
super(props);
this.state = {
items: [], // instantiate initial list here
moreItemsLoading: false,
hasNextPage: true
};
this.loadMore = this.loadMore.bind(this);
}
loadMore() {
// method to fetch newer entries for the list
}
render() {
const { items, moreItemsLoading, hasNextPage } = this.state;
return (
<ListComponent
items={items}
moreItemsLoading={moreItemsLoading}
loadMore={this.loadMore}
hasNextPage={hasNextPage}
/>
);
}
}
export default App;
שיטת loadMore מועברת לרכיב צאצא ListComponent שמכיל את רשימת הטעינה האינסופית. המדד הזה חשוב כי הטעינה האינסופית צריכה להפעיל קריאה חוזרת (callback) כדי לטעון עוד פריטים אחרי שהמשתמש גלל מעבר לנקודה מסוימת.
כך יכול להיראות הקוד ListComponent שיוצר את הרשימה:
import React from 'react';
import { FixedSizeList } from 'react-window';
import InfiniteLoader from "react-window-infinite-loader";
const ListComponent = ({ items, moreItemsLoading, loadMore, hasNextPage }) => {
const Row = ({ index, style }) => (
{/* define the row component using items[index] */}
);
const itemCount = hasNextPage ? items.length + 1 : items.length;
return (
<InfiniteLoader
isItemLoaded={index => index < items.length}
itemCount={itemCount}
loadMoreItems={loadMore}
>
{({ onItemsRendered, ref }) => (
<FixedSizeList
height={500}
width={500}
itemCount={itemCount}
itemSize={120}
onItemsRendered={onItemsRendered}
ref={ref}
>
{Row}
</FixedSizeList>
)}
</InfiniteLoader>
)
};
export default ListComponent;
כאן, הרכיב FixedSizeList עטוף ב-InfiniteLoader.
המאפיינים שמוקצים לרכיב הטעינה הם:
-
isItemLoaded: שיטה שבודקת אם פריט מסוים נטען -
itemCount: מספר הפריטים ברשימה (או הצפוי) loadMoreItems: פונקציית Callback שמחזירה הבטחה (Promise) שמובילה לנתונים נוספים לרשימה
מאפיין render משמש להחזרת פונקציה שרכיב הרשימה משתמש בה כדי לבצע רינדור.
המאפיינים onItemsRendered ו-ref הם מאפיינים שצריך להעביר.
הגלילה ברשימה עשויה להיראות זהה, אבל עכשיו נשלחת בקשה לאחזור של 10 משתמשים מAPI של משתמש אקראי בכל פעם שמתקרבים לסוף הרשימה. כל הפעולות האלה מתבצעות בזמן שמוצג רק 'חלון' אחד של תוצאות בכל פעם.
אם בודקים את index של פריט מסוים, יכול להיות שיוצג מצב טעינה שונה של הפריט, בהתאם לשאלה אם נשלחה בקשה לרשומות חדשות יותר והפריט עדיין בטעינה.
לדוגמה:
const Row = ({ index, style }) => {
const itemLoading = index === items.length;
if (itemLoading) {
// return loading state
} else {
// return item
}
};
סריקת יתר
מאחר שפריטים ברשימה וירטואלית משתנים רק כשהמשתמש גולל, יכול להיות שיופיעו לרגע שטחים ריקים לפני שיוצגו רשומות חדשות יותר. כדי להבין את זה, אפשר לנסות לגלול במהירות בדוגמאות הקודמות במדריך הזה.
כדי לשפר את חוויית המשתמש ברשימות וירטואליות, react-window מאפשר לכם לסרוק פריטים עם המאפיין overscanCount. כך תוכלו להגדיר כמה פריטים מחוץ ל'חלון' הגלוי יוצגו בכל רגע.
<FixedSizeList
//...
overscanCount={4}
>
{...}
</FixedSizeList>
הפונקציה overscanCount פועלת גם ברכיבים FixedSizeList וגם ברכיבים VariableSizeList, וערך ברירת המחדל שלה הוא 1. בהתאם לגודל הרשימה ולגודל של כל פריט, סריקת יתר של יותר מפריט אחד יכולה למנוע הבהוב בולט של שטח ריק כשהמשתמש גולל. עם זאת, סריקה מוגזמת של יותר מדי רשומות עלולה להשפיע לרעה על הביצועים. המטרה של שימוש ברשימה וירטואלית היא למזער את מספר הרשומות למספר שהמשתמש יכול לראות בכל רגע נתון, ולכן כדאי לנסות לשמור על מספר הפריטים שנסרקו מעבר למה שמוצג על המסך נמוך ככל האפשר.
במאפיינים FixedSizeGrid ו-VariableSizeGrid, משתמשים במאפיינים overscanColumnsCount ו-overscanRowsCount כדי לשלוט במספר העמודות והשורות שיוצגו בהתאמה.
סיכום
אם אתם לא בטוחים מאיפה להתחיל להשתמש בווירטואליזציה של רשימות וטבלאות באפליקציה, אתם יכולים לפעול לפי השלבים הבאים:
- מדידת הביצועים של הרינדור והגלילה. במאמר הזה מוסבר איך אפשר להשתמש במדד FPS בכלי הפיתוח ל-Chrome כדי לבדוק את יעילות העיבוד של פריטים ברשימה.
- מומלץ להוסיף
react-windowלכל רשימה ארוכה או רשת שמשפיעה על הביצועים. - אם יש תכונות מסוימות שלא נתמכות ב-
react-window, כדאי להשתמש ב-react-virtualizedאם אי אפשר להוסיף את הפונקציונליות הזו בעצמכם. - אם אתם צריכים לטעון פריטים באופן מדורג בזמן שהמשתמש גולל, עליכם להוסיף את התג
react-window-infinite-loaderלרשימה הווירטואלית. - כדי למנוע הבהוב של תוכן ריק, משתמשים במאפיין
overscanCountעבור הרשימות ובמאפייניםoverscanColumnsCountו-overscanRowsCountעבור הרשתות. לא מומלץ לסרוק יותר מדי רשומות, כי זה ישפיע לרעה על הביצועים.