יצירת רשימה וירטואלית של רשימות גדולות באמצעות חלון תגובה

רשימות וטבלאות גדולות במיוחד יכולות להאט משמעותית את ביצועי האתר. וירטואליזציה יכולה לעזור.

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 כדי לשלוט במספר העמודות והשורות שיש לבצע בהן סריקת יתר בהתאמה.

סיכום

אם אתם לא בטוחים איפה להתחיל בווירטואליזציה של רשימות וטבלאות באפליקציה, בצעו את השלבים הבאים:

  1. מודדים את ביצועי הרינדור והגלילה. במאמר הזה מוסבר איך אפשר להשתמש במד FPS בכלי הפיתוח ל-Chrome כדי לבדוק את היעילות של פריטים ברשימה.
  2. צריך לכלול את השדה react-window ברשימות או במשבצות ארוכות שמשפיעות על הביצועים.
  3. אם יש תכונות מסוימות שלא נתמכות ב-react-window, כדאי להשתמש ב-react-virtualized אם אתם לא יכולים להוסיף את הפונקציונליות הזו בעצמכם.
  4. אם אתם צריכים לטעון פריטים באופן מדורג, בזמן שהמשתמש גולל, עליכם לסגור את הרשימה הווירטואלית עם react-window-infinite-loader.
  5. כדאי להשתמש במאפיין overscanCount לרשימות שלכם ובמאפיינים overscanColumnsCount ו-overscanRowsCount למשבצות כדי למנוע הבזק של תוכן ריק. אל תחריגו יותר מדי רשומות, כי זה יפגע בביצועים.