טעינה עצלה ו-Code Splitting

מבוא

כברירת מחדל, כל קוד האפליקציה נארז לקובץ אחד גדול שהמשתמש מוריד בטעינה הראשונה. ככל שהאפליקציה גדלה, הטעינה מואטת. שלוש הטכניקות בשיעור זה פותרות זאת יחד:

  • Code Splitting - חלוקת הקוד למספר חבילות קטנות (chunks) במקום קובץ ענק אחד
  • Lazy Loading - טעינת קומפוננטה רק ברגע שהיא באמת נדרשת
  • Suspense - הצגת ממשק טעינה זמני בזמן שהקומפוננטה העצלה נטענת

React.lazy

React.lazy עוטף ייבוא דינמי (import()) והופך קומפוננטה לכזו שנטענת רק בעת הצורך. הקוד שלה יופרד אוטומטית לחבילה נפרדת. מכיוון שהטעינה לוקחת רגע, חובה לעטוף את הקומפוננטה ב-Suspense עם ממשק טעינה חלופי:

התחביר

import React, { lazy, Suspense } from 'react';

// במקום import רגיל
// import HeavyComponent from './HeavyComponent';

// טעינה עצלה
const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
    return (
        <Suspense fallback={<div>טוען...</div>}>
            <HeavyComponent />
        </Suspense>
    );
}

Suspense

Suspense מציג תוכן חלופי (fallback) בזמן טעינה.

import { Suspense, lazy } from 'react';

const LazyComponent = lazy(() => import('./LazyComponent'));

function App() {
    return (
        <div>
            <h1>האפליקציה שלי</h1>

            <Suspense fallback={<LoadingSpinner />}>
                <LazyComponent />
            </Suspense>
        </div>
    );
}

function LoadingSpinner() {
    return (
        <div className="spinner-container">
            <div className="spinner"></div>
            <p>טוען...</p>
        </div>
    );
}

Suspense מקונן

function App() {
    return (
        <Suspense fallback={<FullPageLoader />}>
            <Header />

            <Suspense fallback={<ContentLoader />}>
                <MainContent />
            </Suspense>

            <Suspense fallback={<SidebarLoader />}>
                <Sidebar />
            </Suspense>
        </Suspense>
    );
}

Code Splitting עם Routes

הדרך הנפוצה ביותר - חלוקה לפי דפים:

import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';

// טעינה עצלה של כל דף
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));

function App() {
    return (
        <BrowserRouter>
            <Suspense fallback={<PageLoader />}>
                <Routes>
                    <Route path="/" element={<Home />} />
                    <Route path="/about" element={<About />} />
                    <Route path="/dashboard" element={<Dashboard />} />
                    <Route path="/settings" element={<Settings />} />
                </Routes>
            </Suspense>
        </BrowserRouter>
    );
}

function PageLoader() {
    return (
        <div className="page-loader">
            <div className="progress-bar"></div>
        </div>
    );
}

Named Exports

React.lazy מצפה לייבוא ברירת מחדל (default export). כדי לטעון בעצלתיים רכיב שיוצא כ-named export, יש "לעטוף" אותו ולהחזיר אותו כ-default:

// ❌ לא עובד ישירות
const MyComponent = lazy(() => import('./Components').MyComponent);

// ✅ פתרון
const MyComponent = lazy(() => 
    import('./Components').then(module => ({ default: module.MyComponent }))
);

Error Handling

שילוב Suspense עם Error Boundary:

import { lazy, Suspense } from 'react';

const LazyComponent = lazy(() => import('./LazyComponent'));

function App() {
    return (
        <ErrorBoundary fallback={<ErrorMessage />}>
            <Suspense fallback={<Loading />}>
                <LazyComponent />
            </Suspense>
        </ErrorBoundary>
    );
}

class ErrorBoundary extends React.Component {
    state = { hasError: false };

    static getDerivedStateFromError() {
        return { hasError: true };
    }

    render() {
        if (this.state.hasError) {
            return this.props.fallback;
        }
        return this.props.children;
    }
}

Preloading

אפשר לשפר עוד את חוויית המשתמש על ידי טעינה מוקדמת של קומפוננטות שסביר שיידרשו בקרוב - למשל להתחיל לטעון את עמוד לוח הבקרה כבר כשהעכבר מרחף מעל הקישור אליו, כך שכשהמשתמש ילחץ הוא כבר יהיה מוכן:

const LazyDashboard = lazy(() => import('./Dashboard'));

function Navigation() {
    // טעינה מוקדמת כשהעכבר מעל הלינק
    const handleMouseEnter = () => {
        import('./Dashboard'); // מתחיל לטעון ברקע
    };

    return (
        <nav>
            <Link 
                to="/dashboard" 
                onMouseEnter={handleMouseEnter}
            >
                Dashboard
            </Link>
        </nav>
    );
}

Prefetch עם Webpack

// prefetch - עדיפות נמוכה (idle time)
const LazyComponent = lazy(() => 
    import(/* webpackPrefetch: true */ './Component')
);

// preload - עדיפות גבוהה (מיד)
const LazyComponent = lazy(() => 
    import(/* webpackPreload: true */ './Component')
);

Dynamic Import עם תנאי

function FeatureComponent({ type }) {
    const [Component, setComponent] = useState(null);

    useEffect(() => {
        async function loadComponent() {
            let module;

            if (type === 'chart') {
                module = await import('./ChartComponent');
            } else if (type === 'table') {
                module = await import('./TableComponent');
            } else {
                module = await import('./DefaultComponent');
            }

            setComponent(() => module.default);
        }

        loadComponent();
    }, [type]);

    if (!Component) return <Loading />;

    return <Component />;
}

ניתוח Bundle

webpack-bundle-analyzer

npm install --save-dev webpack-bundle-analyzer
// vite.config.js
import { visualizer } from 'rollup-plugin-visualizer';

export default {
    plugins: [
        visualizer({
            open: true,
            filename: 'bundle-stats.html'
        })
    ]
};

Best Practices

1. Route-based Splitting

// מומלץ - כל דף נטען בנפרד
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));

2. Component-based Splitting

// קומפוננטות כבדות
const HeavyEditor = lazy(() => import('./HeavyEditor'));
const ChartLibrary = lazy(() => import('./ChartLibrary'));

3. Feature-based Splitting

// פיצ'רים שלא כולם משתמשים
const AdminPanel = lazy(() => import('./features/AdminPanel'));
const Analytics = lazy(() => import('./features/Analytics'));

סיכום

API שימוש
React.lazy() טעינה דינמית של קומפוננטה
Suspense הצגת fallback בזמן טעינה
import() Dynamic import של מודול
webpackPrefetch טעינה ברקע (idle)
webpackPreload טעינה מוקדמת (מיד)

הקודם: ← Performance
הבא: useReducer →

בדקו את עצמכם

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

  1. מה היתרון של React.lazy?

    1. הקוד של קומפוננטה נטען רק כשצריך אותה, והטעינה הראשונית קטנה
    2. עיצוב טוב יותר
    3. קוד קצר יותר
    4. אין יתרון
    הצגת התשובה

    תשובה א. Code Splitting: כל דף נטען כשנכנסים אליו.

  2. מה חייב לעטוף קומפוננטה עצלה?

    1. div
    2. Router בלבד
    3. Suspense עם fallback
    4. Context
    הצגת התשובה

    תשובה ג. Suspense מציג את ה-fallback עד שהקוד נטען.