לדלג לתוכן

10.5 Performance Optimization תרגול

תרגול - אופטימיזציית ביצועים - Performance Optimization

תרגיל 1 - אופטימיזציית תמונות

נתון גלריית תמונות שטוענת 50 תמונות בבת אחת ללא אופטימיזציה:

function Gallery({ images }: { images: { src: string; title: string }[] }) {
  return (
    <div className="gallery">
      {images.map((img, i) => (
        <div key={i} className="gallery-item">
          <img src={img.src} />
          <p>{img.title}</p>
        </div>
      ))}
    </div>
  );
}

שפרו את הקוד עם:
- שימוש ב-next/image
- lazy loading לכל התמונות מלבד 6 הראשונות
- גדלים רספונסיביים עם sizes
- placeholder מטושטש (blur placeholder)
- שמירת מקום למניעת CLS


תרגיל 2 - פיצול קוד וייבוא דינמי

נתונה אפליקציית דשבורד עם כמה מודולים כבדים:

import MarkdownEditor from './MarkdownEditor'; // 200KB
import ChartLibrary from './ChartLibrary';       // 150KB
import PdfExporter from './PdfExporter';         // 100KB
import DataTable from './DataTable';             // 80KB

function Dashboard() {
  const [activeTab, setActiveTab] = useState('table');

  return (
    <div>
      <nav>
        <button onClick={() => setActiveTab('table')}>טבלה</button>
        <button onClick={() => setActiveTab('chart')}>גרף</button>
        <button onClick={() => setActiveTab('editor')}>עורך</button>
        <button onClick={() => setActiveTab('export')}>ייצוא</button>
      </nav>

      {activeTab === 'table' && <DataTable />}
      {activeTab === 'chart' && <ChartLibrary />}
      {activeTab === 'editor' && <MarkdownEditor />}
      {activeTab === 'export' && <PdfExporter />}
    </div>
  );
}

שפרו את הקוד כך שכל טאב נטען רק כשצריך. הוסיפו loading states מתאימים.


תרגיל 3 - ניתוח ושיפור Bundle

נתון הקוד הבא שמשתמש בספריות כבדות. שפרו את גודל ה-bundle:

import _ from 'lodash';
import moment from 'moment';
import 'moment/locale/he';
import { v4 as uuid } from 'uuid';
import axios from 'axios';

function processData(items: any[]) {
  return _.chain(items)
    .filter(item => moment(item.date).isAfter(moment().subtract(30, 'days')))
    .sortBy('name')
    .map(item => ({
      ...item,
      id: uuid(),
      formattedDate: moment(item.date).locale('he').format('DD MMMM YYYY'),
    }))
    .value();
}

async function fetchData() {
  const response = await axios.get('/api/data');
  return response.data;
}

החליפו את הספריות הכבדות בחלופות קלות או בקוד native. ציינו את החיסכון בגודל bundle לכל שינוי.


תרגיל 4 - אופטימיזציות React

נתונה קומפוננטת רשימת משימות עם בעיות ביצועים. כל שינוי ב-input גורם לרינדור מחדש של כל הרשימה (1000 פריטים):

'use client';

import { useState } from 'react';

interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

function TodoApp() {
  const [todos, setTodos] = useState<Todo[]>(generateTodos(1000));
  const [newTodo, setNewTodo] = useState('');
  const [filter, setFilter] = useState('all');

  const filteredTodos = todos.filter(todo => {
    if (filter === 'active') return !todo.completed;
    if (filter === 'completed') return todo.completed;
    return true;
  });

  const stats = {
    total: todos.length,
    completed: todos.filter(t => t.completed).length,
    active: todos.filter(t => !t.completed).length,
  };

  function toggleTodo(id: number) {
    setTodos(todos.map(t => t.id === id ? { ...t, completed: !t.completed } : t));
  }

  function deleteTodo(id: number) {
    setTodos(todos.filter(t => t.id !== id));
  }

  function addTodo() {
    if (!newTodo.trim()) return;
    setTodos([...todos, { id: Date.now(), text: newTodo, completed: false }]);
    setNewTodo('');
  }

  return (
    <div>
      <h1>רשימת משימות</h1>

      <div>
        <input value={newTodo} onChange={e => setNewTodo(e.target.value)} />
        <button onClick={addTodo}>הוסף</button>
      </div>

      <div>
        <span>סה"כ: {stats.total}</span>
        <span>הושלמו: {stats.completed}</span>
        <span>פעילות: {stats.active}</span>
      </div>

      <div>
        <button onClick={() => setFilter('all')}>הכל</button>
        <button onClick={() => setFilter('active')}>פעילות</button>
        <button onClick={() => setFilter('completed')}>הושלמו</button>
      </div>

      <ul>
        {filteredTodos.map(todo => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => toggleTodo(todo.id)}
            />
            <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
              {todo.text}
            </span>
            <button onClick={() => deleteTodo(todo.id)}>מחק</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

שפרו עם: memo, useMemo, useCallback, וירטואליזציה (react-window).


תרגיל 5 - אסטרטגיית Caching

תכננו ויישמו אסטרטגיית caching מלאה לאתר חדשות:

  1. דף הבית - מתעדכן כל 5 דקות
  2. דף מאמר - מתעדכן כל שעה
  3. תמונות - cache ארוך (חודש)
  4. קבצי JS/CSS סטטיים - cache ארוך מאוד (שנה, immutable)
  5. API responses - cache קצר עם stale-while-revalidate

כתבו:
- הגדרות Cache-Control ב-next.config.js
- הגדרות revalidation בדפי Next.js
- הגדרות cache ב-fetch calls


תרגיל 6 - אופטימיזציית פונטים

נתון אתר שטוען 4 פונטים מ-Google Fonts:
- Rubik (עברית) - Regular, Bold
- Inter (אנגלית) - Regular, Medium, Bold
- Fira Code (קוד) - Regular
- Open Sans (תוכן) - Regular, Bold

הבעיות:
- כל הפונטים נטענים מ-Google Fonts CDN
- אין display strategy
- האתר מציג FOUT ו-CLS

שפרו:
- השתמשו ב-next/font
- הגדירו CSS variables
- בטלו את Open Sans (מיותר - Rubik ו-Inter מספיקים)
- הגדירו display strategy ו-fallbacks
- טענו רק את ה-subsets הנדרשים


שאלות

  1. מה ההבדל בין loading="lazy" ל-priority ב-next/image? מתי משתמשים בכל אחד?
  2. מה זה Tree Shaking ולמה זה לא עובד עם CommonJS?
  3. הסבירו את ההבדל בין React.memo, useMemo, ו-useCallback. מתי משתמשים בכל אחד?
  4. מה זה stale-while-revalidate ואיך זה משפר את חווית המשתמש?
  5. למה וירטואליזציה חשובה ומה החיסרון שלה?