אופטימיזציית ביצועים
מבוא
React מהירה כברירת מחדל, אך באפליקציות גדולות רינדורים מיותרים עלולים להצטבר ולפגוע בחוויית המשתמש. בשיעור זה נכיר את שלושת הכלים המרכזיים לאופטימיזציה - React.memo, useMemo ו-useCallback. חשוב לזכור כלל אצבע: אין לבצע אופטימיזציה בטרם עת. משתמשים בכלים האלה כשמזהים בעיית ביצועים אמיתית, לא באופן גורף.
React.memo
React.memo עוטף קומפוננטה ו"מזכיר" את התוצאה האחרונה שלה: אם ה-props לא השתנו מהרינדור הקודם, React מדלגת על הרינדור ומשתמשת בתוצאה השמורה. זה יעיל במיוחד לקומפוננטות "כבדות" שמקבלות את אותם props אך מתרנדרות שוב ושוב בגלל שינוי בהורה:
הבעיה
function Parent() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
return (
<div>
<button onClick={() => setCount(c => c + 1)}>
{count}
</button>
{/* ExpensiveList מתרנדר בכל שינוי של count! */}
<ExpensiveList items={items} />
</div>
);
}
function ExpensiveList({ items }) {
console.log('ExpensiveList rendered!'); // נקרא בכל רינדור
return <ul>{items.map(item => <li key={item}>{item}</li>)}</ul>;
}
הפתרון
// עטיפה ב-React.memo
const ExpensiveList = React.memo(function ExpensiveList({ items }) {
console.log('ExpensiveList rendered!'); // נקרא רק כש-items משתנה
return <ul>{items.map(item => <li key={item}>{item}</li>)}</ul>;
});
Custom Comparison
const MemoizedComponent = React.memo(
function MyComponent({ user, onClick }) {
return <div onClick={onClick}>{user.name}</div>;
},
// Custom comparison function
(prevProps, nextProps) => {
// return true = לא לרנדר מחדש
// return false = לרנדר מחדש
return prevProps.user.id === nextProps.user.id;
}
);
useMemo
אם React.memo מזכיר קומפוננטה שלמה, useMemo מזכיר תוצאה של חישוב בודד בתוך קומפוננטה. הוא שומר את הערך המחושב ומריץ את החישוב מחדש רק כשאחת התלויות משתנה - שימושי לחישובים כבדים או לייצוב אובייקטים ומערכים שמועברים כ-props:
מתי להשתמש?
- חישובים כבדים
- יצירת אובייקטים/מערכים גדולים
- מניעת רינדורים מיותרים ב-children
התחביר
const memoizedValue = useMemo(() => {
return computeExpensiveValue(a, b);
}, [a, b]); // מחושב מחדש רק כש-a או b משתנים
דוגמאות
function ProductList({ products, filter, sort }) {
// ❌ מחושב מחדש בכל רינדור
const filteredProducts = products
.filter(p => p.category === filter)
.sort((a, b) => sort === 'price' ? a.price - b.price : a.name.localeCompare(b.name));
// ✅ מחושב מחדש רק כשצריך
const filteredProducts = useMemo(() => {
console.log('Filtering and sorting...');
return products
.filter(p => p.category === filter)
.sort((a, b) => sort === 'price' ? a.price - b.price : a.name.localeCompare(b.name));
}, [products, filter, sort]);
return (
<ul>
{filteredProducts.map(p => <li key={p.id}>{p.name}</li>)}
</ul>
);
}
מניעת רינדור ילדים
function Parent() {
const [count, setCount] = useState(0);
// ❌ אובייקט חדש בכל רינדור
const style = { color: 'red', fontSize: 20 };
// ✅ אותו reference
const style = useMemo(() => ({ color: 'red', fontSize: 20 }), []);
return (
<div>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<MemoizedChild style={style} />
</div>
);
}
useCallback
useCallback הוא המקבילה של useMemo עבור פונקציות: הוא שומר את אותה פונקציה בין רינדורים ויוצר חדשה רק כשהתלויות משתנות. זה קריטי כשמעבירים פונקציה לקומפוננטת ילד עטופה ב-React.memo, שכן פונקציה חדשה בכל רינדור הייתה מבטלת את היתרון שלה:
למה זה חשוב?
function Parent() {
const [count, setCount] = useState(0);
// ❌ פונקציה חדשה בכל רינדור
const handleClick = () => {
console.log('clicked');
};
// ✅ אותה פונקציה (אותו reference)
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
return <MemoizedButton onClick={handleClick} />;
}
const MemoizedButton = React.memo(function Button({ onClick }) {
console.log('Button rendered');
return <button onClick={onClick}>לחץ</button>;
});
עם תלויות
function SearchComponent({ query }) {
const [results, setResults] = useState([]);
// פונקציה שתלויה ב-query
const search = useCallback(async () => {
const data = await fetchResults(query);
setResults(data);
}, [query]); // נוצרת מחדש רק כש-query משתנה
useEffect(() => {
search();
}, [search]);
}
השוואה: useMemo vs useCallback
// useMemo - שומר ערך
const memoizedValue = useMemo(() => computeValue(a, b), [a, b]);
// useCallback - שומר פונקציה
const memoizedFn = useCallback(() => doSomething(a, b), [a, b]);
// useCallback זה בעצם:
const memoizedFn = useMemo(() => () => doSomething(a, b), [a, b]);
מתי להשתמש?
| כלי | מתי להשתמש |
|---|---|
React.memo |
קומפוננטה שמתרנדרת הרבה עם אותם props |
useMemo |
חישוב כבד / אובייקט שמועבר כ-prop |
useCallback |
פונקציה שמועברת ל-memoized child |
כלל אצבע
// ❌ לא צריך - פשוט מדי
const doubled = useMemo(() => count * 2, [count]);
// ✅ כן צריך - חישוב כבד
const filteredList = useMemo(() =>
hugeList.filter(item => complexFilter(item, criteria)),
[hugeList, criteria]
);
טעויות נפוצות
1. Memoization מיותרת
// ❌ לא יעזור - הקומפוננטה ההורה לא memo
const MemoizedChild = React.memo(Child);
function Parent() {
return <MemoizedChild data={data} />;
}
2. Dependencies חסרות
// ❌ שגוי - count לא ב-dependencies
const handleClick = useCallback(() => {
console.log(count); // תמיד הערך הראשוני!
}, []);
// ✅ נכון
const handleClick = useCallback(() => {
console.log(count);
}, [count]);
3. אובייקט inline
// ❌ אובייקט חדש בכל רינדור - memo לא עוזר
<MemoizedChild options={{ sort: true }} />
// ✅ useMemo
const options = useMemo(() => ({ sort: true }), []);
<MemoizedChild options={options} />
כלי Debug
React DevTools Profiler
// הוספת displayName לזיהוי קל
const MemoizedComponent = React.memo(function MyComponent(props) {
return <div>{props.value}</div>;
});
MemoizedComponent.displayName = 'MemoizedComponent';
Why Did You Render
npm install @welldone-software/why-did-you-render
// wdyr.js
import React from 'react';
if (process.env.NODE_ENV === 'development') {
const whyDidYouRender = require('@welldone-software/why-did-you-render');
whyDidYouRender(React, { trackAllPureComponents: true });
}
// Component.jsx
MyComponent.whyDidYouRender = true;
סיכום
| Hook/API | תחביר | שימוש |
|---|---|---|
React.memo |
React.memo(Component) |
מניעת רינדור |
useMemo |
useMemo(() => value, [deps]) |
שמירת ערך |
useCallback |
useCallback(fn, [deps]) |
שמירת פונקציה |
הקודם: ← React Router
הבא: Lazy Loading & Suspense →
בדקו את עצמכם
נסו לענות לבד לפני שאתם פותחים את התשובה.
-
מה עושה
React.memo?הצגת התשובה
תשובה ב. שימושי לקומפוננטות כבדות שמקבלות אותם props.
-
מה ההבדל בין useMemo ל-useCallback?
הצגת התשובה
תשובה ב. שניהם מחשבים מחדש רק כשהתלויות משתנות. React Compiler עושה זאת היום אוטומטית ברוב המקרים.