טיפ ריאקט: רינדור רשימות ארוכות
ריאקט נותן לנו הרגשה שהכל אפשרי. שאם רק ניתן לו לעשות את העבודה שלו נוכל לקבל ביצועים מצוינים ואפליקציה שמשתמשים יהנו ממנה. המציאות לא תמיד תואמת את התחושה. מקום אחד שתמיד מפתיע מתכנתים הוא רשימות ארוכות, והאופן בו ריאקט מטפל (נו, מנסה לטפל) בכל הסיבוך סביב זה.
1. מה שבור ברשימות ארוכות
הסנדבוקס הבא כולל רשימה ארוכה:
לטובת מי שאין לו אייפריימים הנה הקוד עצמו:
import "./styles.css";
import { useState } from "react";
import _ from "lodash";
function HilightedWord(props) {
const { hl, text } = props;
return Array.from(text).map((ch) =>
hl.includes(ch) ? <span className="hl">{ch}</span> : <span>{ch}</span>
);
}
export default function App() {
const [text, setText] = useState("");
return (
<div className="App">
<div>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
/>
</div>
<p>You typed {text}</p>
<ul>
{_.range(5000).map((i) => (
<li key={i}>
<HilightedWord hl={text} text="Hello world" />
</li>
))}
</ul>
</div>
);
}
יש פה רשימה של 5000 פריטים ותיבת חיפוש מעליהם. כשאנחנו כותבים טקסט בתיבה האותיות מהטקסט שמופיעות בפריטים שברשימה יודגשו בצבע אדום. החלק המרשים לרעה פה הוא כמה זמן לוקח לעמוד להתמודד עם הלחיצה שלי. בואו רק נגיד שיש אנשים שיספיקו לסיים כוס אספרסו עד שיראו את הטקסט מתעדכן בתיבה.
זה קורה בגלל שכל שינוי טקסט בתיבה גורם לריאקט ללכת ולהפעיל render על כל ה 5,000 קומפוננטות שברשימה כי אולי חלקן התעדכנו. ואין ממש דרך לצאת מזה: החישוב של מה מתעדכן ומה לא צריך לקרות מתישהו בשביל שהממשק יראה את המידע הנכון. נכון?
2. והטיפ להיום- החישוב לא חייב לקרות עכשיו
נו, כמעט נכון. כן החישוב צריך לקרות אבל אף אחד לא אמר שהוא צריך לקרות עכשיו. זה נכון שיש 5000 פריטים ברשימה אבל בקושי 50 מהם מוצגים בכל זמן נתון. טריק אחד שאפשר לנסות הוא להפעיל render רק על הפריטים שמשתמש רואה, ולהפעיל את ה render על האחרים רק כשינסו לגלול אותם למסך. זאת בדיוק המטרה של ספריית react-window.
הקוד עם הספריה נראה כך:
import React, { useState } from "react";
import { FixedSizeList as List } from "react-window";
import AutoSizer from "react-virtualized-auto-sizer";
import "./styles.css";
function HilightedWord(props) {
const { hl, text } = props;
return Array.from(text).map((ch) =>
hl.includes(ch) ? <span className="hl">{ch}</span> : <span>{ch}</span>
);
}
export default function Example() {
const [text, setText] = useState("");
return (
<>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
/>
<AutoSizer>
{({ height, width }) => (
<List
className="List"
height={height}
itemCount={1000}
itemSize={35}
width={width}
>
{({ index, style }) => (
<div style={style}>
<HilightedWord hl={text} text={`Hello World`} />
</div>
)}
</List>
)}
</AutoSizer>
</>
);
}
השינוי המרכזי מהקוד שאני כתבתי הוא שהפעם זו react-window שיוצרת את הרשימה ולכן היא גם תהיה אחראית על רינדור הפריטים. התוצאה היא תיקון מאוד פשוט לבעיית הביצועים. אתם מוזמנים לשחק עם הגירסה המתוקנת בקודסנדבוקס כאן:
הפעם התגובה בתיבת החיפוש היא מיידית וגם הדגשת האותיות. הדבר שכן לוקח זמן הוא גלילה מאחר והעברנו את החישוב לאירוע scroll. אבל זה כבר כמעט ולא מורגש.