לדלג לתוכן

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

Cache-Control: public, max-age=60, stale-while-revalidate=3600
  • 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