10.5 Performance Optimization הרצאה
אופטימיזציית ביצועים - Performance Optimization¶
בשיעור זה נלמד טכניקות מעשיות לשיפור ביצועי אפליקציות פרונטאנד. נכסה אופטימיזציית תמונות, פיצול קוד, caching, ואופטימיזציות ספציפיות ל-React.
אופטימיזציית תמונות - Image Optimization¶
תמונות הן לרוב המשאב הכבד ביותר בדף. אופטימיזציה נכונה יכולה לחסוך עשרות אחוזים מזמן הטעינה.
פורמטים מודרניים¶
| פורמט | שימוש | יתרונות |
|---|---|---|
| WebP | תמונות כלליות | 25-35% קטן מ-JPEG, שקיפות |
| AVIF | תמונות כלליות | 50% קטן מ-JPEG, הכי יעיל |
| SVG | אייקונים ולוגואים | וקטורי, קל, scalable |
| PNG | תמונות עם שקיפות | lossless, איכות גבוהה |
| JPEG | תמונות צילום | נתמך בכל מקום |
טעינה עצלה - Lazy Loading¶
<!-- native lazy loading -->
<img src="product.jpg" alt="מוצר" loading="lazy" />
<!-- eager loading לתמונות above the fold -->
<img src="hero.jpg" alt="hero" loading="eager" />
תמונות רספונסיביות עם srcset¶
<!-- הדפדפן בוחר את הגודל המתאים למסך -->
<img
src="product-800.jpg"
srcset="
product-400.jpg 400w,
product-800.jpg 800w,
product-1200.jpg 1200w
"
sizes="(max-width: 600px) 400px, (max-width: 900px) 800px, 1200px"
alt="מוצר"
/>
קומפוננטת next/image¶
import Image from 'next/image';
// תמונה בסיסית עם אופטימיזציה אוטומטית
<Image
src="/product.jpg"
alt="מוצר"
width={800}
height={600}
/>
// תמונה רספונסיבית שממלאת את הקונטיינר
<div style={{ position: 'relative', width: '100%', aspectRatio: '16/9' }}>
<Image
src="/hero.jpg"
alt="תמונת גיבור"
fill
sizes="100vw"
priority
style={{ objectFit: 'cover' }}
/>
</div>
// תמונה מרחוק עם דומיין מאושר
// next.config.js:
// images: { remotePatterns: [{ hostname: 'cdn.example.com' }] }
<Image
src="https://cdn.example.com/product.jpg"
alt="מוצר"
width={400}
height={300}
/>
מה next/image עושה אוטומטית:
- המרה לפורמט WebP/AVIF
- יצירת srcset עם גדלים שונים
- lazy loading אוטומטי (אלא אם מוגדר priority)
- מניעת CLS עם שמירת מקום
- אופטימיזציית גודל לפי הצורך
פיצול קוד וייבוא דינמי - Code Splitting and Dynamic Imports¶
למה פיצול קוד חשוב¶
- ללא פיצול, כל ה-JavaScript נטען ב-bundle אחד
- המשתמש מוריד קוד של דפים שהוא אולי לא יבקר בהם
- פיצול מאפשר טעינה רק של הקוד הנדרש
ייבוא דינמי ב-React¶
import { lazy, Suspense } from 'react';
// במקום: import HeavyChart from './HeavyChart';
const HeavyChart = lazy(() => import('./HeavyChart'));
function Dashboard() {
return (
<div>
<h1>דשבורד</h1>
<Suspense fallback={<div>טוען גרף...</div>}>
<HeavyChart data={chartData} />
</Suspense>
</div>
);
}
ייבוא דינמי ב-Next.js¶
import dynamic from 'next/dynamic';
// קומפוננטה שנטענת רק בלקוח
const RichTextEditor = dynamic(() => import('./RichTextEditor'), {
ssr: false, // לא מרנדר בצד השרת
loading: () => <div style={{ height: '300px' }}>טוען עורך...</div>,
});
// קומפוננטה כבדה שנטענת רק כשצריך
const PdfViewer = dynamic(() => import('./PdfViewer'), {
loading: () => <p>טוען מציג PDF...</p>,
});
function DocumentPage({ doc }) {
return (
<main>
<h1>{doc.title}</h1>
<RichTextEditor content={doc.content} />
{doc.hasPdf && <PdfViewer url={doc.pdfUrl} />}
</main>
);
}
פיצול לפי נתיב - Route-Based Splitting¶
- ב-Next.js App Router, כל דף (page.tsx) הוא chunk נפרד אוטומטית
- אין צורך בהגדרה מיוחדת - זה קורה מהקומפיילר
// כל page.tsx הוא bundle נפרד
app/
├── page.tsx → chunk-main.js
├── about/page.tsx → chunk-about.js
├── blog/page.tsx → chunk-blog.js
└── products/page.tsx → chunk-products.js
ניתוח Bundle - Bundle Analysis¶
# התקנת webpack-bundle-analyzer
npm install --save-dev @next/bundle-analyzer
# הפעלה
ANALYZE=true npm run build
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
module.exports = withBundleAnalyzer({
// שאר ההגדרות
});
מה לחפש בניתוח¶
- ספריות גדולות שאפשר להחליף (למשל moment.js -> date-fns או dayjs)
- קוד שנטען אבל לא משמש
- ספריות שנטענות ב-bundles מרובים (duplication)
- פונקציות שמייבאים ספרייה שלמה בשבילן
// רע - מייבא את כל lodash (71KB)
import _ from 'lodash';
const sorted = _.sortBy(items, 'name');
// טוב - מייבא רק את הפונקציה הנדרשת (2KB)
import sortBy from 'lodash/sortBy';
const sorted = sortBy(items, 'name');
// הכי טוב - כותבים בעצמנו או משתמשים ב-native
const sorted = [...items].sort((a, b) => a.name.localeCompare(b.name));
ניעור עצים - Tree Shaking¶
- תהליך אוטומטי שמסיר קוד שלא משתמשים בו מה-bundle
- עובד רק עם ES Modules (import/export), לא עם CommonJS (require)
- וודאו שספריות תומכות ב-tree shaking (בדקו אם יש שדה "sideEffects" ב-package.json)
// tree shaking עובד עם named imports
import { Button, Card } from '@/components/ui';
// רק Button ו-Card ייכללו ב-bundle
// tree shaking לא עובד עם CommonJS
const { Button, Card } = require('@/components/ui');
// כל הספרייה תיכלל
// package.json - הגדרת sideEffects
{
"sideEffects": false
}
// או ציון קבצים ספציפיים עם side effects
{
"sideEffects": ["*.css", "./src/polyfills.ts"]
}
אסטרטגיות Caching¶
Browser Cache עם Cache-Control¶
// next.config.js - כותרות cache
module.exports = {
async headers() {
return [
{
// קבצים סטטיים עם hash - cache לשנה
source: '/_next/static/(.*)',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable',
},
],
},
{
// תמונות מותאמות - cache לשבוע
source: '/_next/image(.*)',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=604800, stale-while-revalidate=86400',
},
],
},
{
// דפי HTML - cache קצר
source: '/(.*)',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=0, must-revalidate',
},
],
},
];
},
};
CDN - Content Delivery Network¶
- CDN מפיץ את התוכן לשרתים ברחבי העולם
- המשתמש מקבל את התוכן מהשרת הקרוב אליו
- Vercel, Cloudflare, AWS CloudFront הם CDN-ים נפוצים
- ב-Next.js על Vercel, CDN מוגדר אוטומטית
תבנית stale-while-revalidate¶
max-age=60- הקאש תקף ל-60 שניותstale-while-revalidate=3600- אחרי 60 שניות, מחזיר את הגרסה הישנה מהקאש ובמקביל מרענן ברקע- התוצאה: המשתמש תמיד מקבל תשובה מהירה
// ב-Next.js - revalidation
// ISR - Incremental Static Regeneration
export const revalidate = 60; // רענון כל 60 שניות
// או ברמת fetch
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 }, // רענון כל שעה
});
אופטימיזציית פונטים - Font Optimization¶
בעיות עם פונטים¶
- פונטים חיצוניים מוסיפים בקשות רשת
- FOIT (Flash of Invisible Text) - טקסט נעלם עד שהפונט נטען
- FOUT (Flash of Unstyled Text) - טקסט מוצג בפונט אחר ואז מתחלף
- גורם ל-CLS כשהפונט מתחלף
אופטימיזציה עם next/font¶
// app/layout.tsx
import { Inter, Rubik } from 'next/font/google';
// פונט אנגלי
const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--font-inter',
});
// פונט עברי
const rubik = Rubik({
subsets: ['hebrew', 'latin'],
display: 'swap',
variable: '--font-rubik',
});
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="he" dir="rtl" className={`${inter.variable} ${rubik.variable}`}>
<body>{children}</body>
</html>
);
}
/* שימוש בפונט כ-CSS Variable */
body {
font-family: var(--font-rubik), Arial, sans-serif;
}
code {
font-family: var(--font-inter), monospace;
}
מה next/font עושה:
- מוריד את קבצי הפונט בזמן build ושומר אותם מקומית (self-hosting)
- מבטל בקשות רשת לגוגל פונטס
- משתמש ב-CSS size-adjust למניעת CLS
- display: 'swap' מציג טקסט מיד עם fallback
פונט מקומי¶
import localFont from 'next/font/local';
const heebo = localFont({
src: [
{ path: './fonts/Heebo-Regular.woff2', weight: '400' },
{ path: './fonts/Heebo-Bold.woff2', weight: '700' },
],
display: 'swap',
variable: '--font-heebo',
});
צמצום זמן הרצת JavaScript¶
זיהוי JavaScript כבד¶
// שימוש ב-Chrome DevTools Performance panel:
// 1. הקליטו ביצועים
// 2. חפשו Long Tasks (מסומנים באדום - מעל 50ms)
// 3. זהו את הפונקציות שצורכות הכי הרבה זמן
טכניקות צמצום¶
// 1. Web Worker לחישובים כבדים
// worker.ts
self.onmessage = (e) => {
const result = processLargeDataset(e.data);
self.postMessage(result);
};
// component.tsx
function DataProcessor() {
const worker = useMemo(
() => new Worker(new URL('./worker.ts', import.meta.url)),
[]
);
useEffect(() => {
worker.onmessage = (e) => setResult(e.data);
return () => worker.terminate();
}, [worker]);
return <button onClick={() => worker.postMessage(data)}>עבד</button>;
}
// 2. פיצול משימות ארוכות עם scheduler.yield
async function processItems(items: Item[]) {
for (let i = 0; i < items.length; i++) {
processItem(items[i]);
// כל 100 פריטים, מחזירים שליטה לדפדפן
if (i % 100 === 0) {
await new Promise(resolve => setTimeout(resolve, 0));
}
}
}
// 3. דחיית סקריפטים לא קריטיים
// next.config.js
module.exports = {
async headers() {
return [
{
source: '/(.*)',
headers: [
// טעינת analytics אחרי הדף
{ key: 'Link', value: '</analytics.js>; rel=preload; as=script; fetchpriority=low' },
],
},
];
},
};
אופטימיזציות ספציפיות ל-React¶
React.memo - מניעת רינדור מיותר¶
import { memo } from 'react';
// רק מרנדר מחדש כשה-props משתנים
const ProductCard = memo(function ProductCard({
name,
price,
image,
}: {
name: string;
price: number;
image: string;
}) {
return (
<div className="product-card">
<img src={image} alt={name} />
<h3>{name}</h3>
<p>{price} ₪</p>
</div>
);
});
useMemo - שמירת ערכים מחושבים בזיכרון¶
import { useMemo } from 'react';
function ProductList({ products, filter }: { products: Product[]; filter: string }) {
// הסינון מתבצע רק כשהמוצרים או הפילטר משתנים
const filteredProducts = useMemo(() => {
return products.filter(p => p.name.includes(filter));
}, [products, filter]);
return (
<ul>
{filteredProducts.map(p => <li key={p.id}>{p.name}</li>)}
</ul>
);
}
useCallback - שמירת פונקציות בזיכרון¶
import { useCallback, useState } from 'react';
function ParentComponent() {
const [items, setItems] = useState<Item[]>([]);
// הפונקציה לא נוצרת מחדש בכל רינדור
const handleDelete = useCallback((id: number) => {
setItems(prev => prev.filter(item => item.id !== id));
}, []);
return (
<ul>
{items.map(item => (
<MemoizedItem key={item.id} item={item} onDelete={handleDelete} />
))}
</ul>
);
}
const MemoizedItem = memo(function Item({
item,
onDelete,
}: {
item: Item;
onDelete: (id: number) => void;
}) {
return (
<li>
{item.name}
<button onClick={() => onDelete(item.id)}>מחק</button>
</li>
);
});
וירטואליזציה - Virtualization¶
// שימוש ב-react-window לרשימות ארוכות
import { FixedSizeList as List } from 'react-window';
interface Item {
id: number;
name: string;
}
function VirtualizedList({ items }: { items: Item[] }) {
return (
<List
height={600} // גובה הקונטיינר
itemCount={items.length}
itemSize={60} // גובה כל פריט
width="100%"
>
{({ index, style }) => (
<div style={style}>
<div className="list-item">
{items[index].name}
</div>
</div>
)}
</List>
);
}
- וירטואליזציה מרנדרת רק את הפריטים שנראים על המסך
- במקום 10,000 אלמנטי DOM, יש רק 15-20
- חיוני לרשימות ארוכות, טבלאות גדולות, וגריד עם הרבה פריטים
רשימת בדיקות ביצועים¶
תמונות:
[ ] פורמט WebP/AVIF
[ ] lazy loading לתמונות מתחת ל-fold
[ ] priority לתמונות above the fold
[ ] srcset / sizes מוגדרים
[ ] next/image בשימוש
JavaScript:
[ ] code splitting / dynamic imports
[ ] tree shaking עובד
[ ] אין ספריות מיותרות ב-bundle
[ ] Web Workers לחישובים כבדים
[ ] Long Tasks מזוהים ומפוצלים
Cache:
[ ] Cache-Control headers מוגדרים
[ ] CDN מוגדר
[ ] stale-while-revalidate בשימוש
[ ] ISR / revalidation מוגדר
פונטים:
[ ] next/font או self-hosted
[ ] display: swap
[ ] רק subsets נדרשים
React:
[ ] memo לקומפוננטות כבדות
[ ] useMemo לחישובים יקרים
[ ] useCallback לפונקציות שמועברות כ-props
[ ] וירטואליזציה לרשימות ארוכות
סיכום¶
- אופטימיזציית תמונות: פורמטים מודרניים, lazy loading, srcset, next/image
- פיצול קוד: dynamic imports, route-based splitting, lazy loading
- ניתוח bundle: webpack-bundle-analyzer, זיהוי ספריות כבדות
- Tree shaking: שימוש ב-ES Modules, named imports
- Caching: Cache-Control headers, CDN, stale-while-revalidate
- פונטים: next/font, display swap, self-hosting
- צמצום JavaScript: Web Workers, פיצול משימות, דחיית סקריפטים
- אופטימיזציות React: memo, useMemo, useCallback, virtualization