אופטימיזציית ביצועים

מבוא

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 →

בדקו את עצמכם

נסו לענות לבד לפני שאתם פותחים את התשובה.

  1. מה עושה React.memo?

    1. מאיץ fetch
    2. מדלג על רינדור מחדש של קומפוננטה אם ה-props לא השתנו
    3. שומר state
    4. מוחק קומפוננטות
    הצגת התשובה

    תשובה ב. שימושי לקומפוננטות כבדות שמקבלות אותם props.

  2. מה ההבדל בין useMemo ל-useCallback?

    1. useMemo רק למחרוזות
    2. useMemo שומר ערך מחושב, useCallback שומר פונקציה
    3. useCallback לחישובים
    4. אין הבדל
    הצגת התשובה

    תשובה ב. שניהם מחשבים מחדש רק כשהתלויות משתנות. React Compiler עושה זאת היום אוטומטית ברוב המקרים.