טבלאות ורשימות גדולות במיוחד עלולות להאט באופן משמעותי את ביצועי האתר. וירטואליזציה יכולה לעזור!
react-window
היא ספרייה שמאפשרת להציג רשימות גדולות ביעילות.
דוגמה לרשימת 1,000 שורות שמוצגת באמצעות 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
אחראי לכך.
בדוגמה לתקלה שמוצגת קודם במאמר הזה יש דוגמה לרכיב FixedSizeList
.
מתי כדאי להשתמש ברשימות בגדלים משתנים
משתמשים ברכיב 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;
ההטמעה הבאה מציגה דוגמה לרכיב הזה.
הפונקציה של גודל הפריט שהועברה למאפיין itemSize
משנה באופן אקראי את גובה השורות בדוגמה הזו. עם זאת, באפליקציה אמיתית צריכה להיות לוגיקה בפועל שמגדירה את הגדלים של כל פריט. באופן אידיאלי, צריך לחשב את הגדלים האלה על סמך נתונים או לקבל אותם מ-API.
רשתות
react-window
תומך גם בווירטואליזציה של רשימות או רשתות מרובות-מימדים. בהקשר הזה, ה'חלון' של התוכן הגלוי משתנה כשהמשתמש גולל לרוחב וגם לאורך.
באופן דומה, אפשר להשתמש גם ברכיב FixedSizeGrid
וגם ברכיב VariableSizeGrid
, בהתאם לאפשרות שהגודל של פריטים ספציפיים ברשימה ישתנה.
- ב-
FixedSizeGrid
, ה-API הוא בערך זהה, אבל צריך לייצג את הגבהים, הרוחבים ומספרי הפריטים גם בעמודות וגם בשורות. - ב-
VariableSizeGrid
, אפשר לשנות את רוחב העמודות ואת גובה השורות על ידי העברת פונקציות במקום ערכים ל-props המתאימים.
כדאי לעיין במסמכי העזרה כדי לראות דוגמאות לרשתות וירטואליות.
טעינה מדורגת בזמן גלילה
באתרים רבים, כדי לשפר את הביצועים, המערכת ממתינה לטעינת הפריטים ברשימת פריטים ארוכה ולעיבוד שלהם עד שהמשתמש גולל למטה. הטכניקה הזו, שנקראת בדרך כלל 'טעינה אינסופית', מוסיפה צמתים חדשים של 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
: פונקציית קריאה חוזרת שמחזירה הבטחה שמתקבלים ממנה נתונים נוספים לרשימה
פרמטר ה-render משמש להחזרת פונקציה שבה רכיב הרשימה משתמש כדי לבצע רינדור.
גם המאפיין onItemsRendered
וגם המאפיין ref
הם מאפיינים שצריך להעביר.
הדוגמה הבאה מראה איך טעינה אינסופית יכולה לעבוד עם רשימה וירטואלית.
גלילה למטה ברשימה עשויה להיראות זהה, אבל עכשיו מתבצעת בקשה לאחזר 10 משתמשים מ-random user 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
לרשתות כדי למנוע הבהוב של תוכן ריק. אל תסרקו יותר מדי רשומות, כי זה ישפיע לרעה על הביצועים.