טעינה עצלה ו-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 →
בדקו את עצמכם
נסו לענות לבד לפני שאתם פותחים את התשובה.
-
מה היתרון של
React.lazy?הצגת התשובה
תשובה א. Code Splitting: כל דף נטען כשנכנסים אליו.
-
מה חייב לעטוף קומפוננטה עצלה?
הצגת התשובה
תשובה ג. Suspense מציג את ה-fallback עד שהקוד נטען.