רשימות וטבלאות גדולות במיוחד יכולות להאט משמעותית את ביצועי האתר. וירטואליזציה יכולה לעזור.
react-window
היא ספרייה שמאפשרת לעבד רשימות גדולות ביעילות.
הנה דוגמה לרשימה שמכילה 1,000 שורות שמעובדות באמצעות react-window
. כדאי לנסות לגלול מהר ככל האפשר.
למה זה שימושי?
יכול להיות שבמקרים מסוימים תצטרכו להציג טבלה גדולה או רשימה גדולה שמכילה שורות רבות. טעינה של כל פריט ברשימה כזו יכולה להשפיע באופן משמעותי על הביצועים.
ווירטואליזציה של רשימות, או 'windowing', היא הרעיון של רינדור רק של מה שגלוי למשתמש. מספר הרכיבים שמוצגים בהתחלה הוא קבוצת משנה קטנה מאוד של כל הרשימה, וה "חלון" של התוכן הגלוי עובר כשהמשתמש ממשיך לגלול. כך אפשר לשפר את ביצועי הרינדור והגלילה של הרשימה.
צומתי DOM שיוצאים מה'חלון' עוברים מיחזור או מוחלפים מיד ברכיבים חדשים יותר כשהמשתמש גולל ברשימה. כך המספר של כל הרכיבים המעובדים מותאם לגודל החלון.
חלון תגובה
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
, אפשר לשנות גם את רוחב העמודות וגם את גובה השורות על ידי העברת פונקציות במקום ערכים לתכונות המתאימות.
במסמכי התיעוד מפורטות דוגמאות לרשתות וירטואליות.
טעינה עצלה בזמן גלילה
הרבה אתרים ממתינים לטעינה ולרינדור של פריטים ברשימה ארוכה עד שהמשתמש גולל למטה כדי לשפר את הביצועים. השיטה הזו, שנקראת בדרך כלל 'טעינה אינסופית', מוסיפה צומתי 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) שמחזירה הבטחה שמובילה לנתונים נוספים עבור הרשימה
תוסף עיבוד משמש להחזרת פונקציה שרכיב הרשימה משתמש בה כדי לבצע רינדור.
גם המאפיינים 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
למשבצות כדי למנוע הבזק של תוכן ריק. אל תחריגו יותר מדי רשומות, כי זה יפגע בביצועים.