טבלאות ורשימות גדולות במיוחד עלולות להאט באופן משמעותי את ביצועי האתר. וירטואליזציה יכולה לעזור!
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
אחראי לכך.
הדוגמה ל-Glitch שמוצגת למעלה היא דוגמה לרכיב 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
באמצעות חבילת react-window-infinite-loader
נפרדת.
קטע הקוד הבא מציג דוגמה למצב שמנוהל ברכיב הורה 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
ברשתות. אל תסרקו יותר מדי רשומות, כי זה ישפיע לרעה על הביצועים.