לדלג לתוכן

11.7 מונוריפו וביצועים מתקדמים הרצאה

מונוריפו וביצועים מתקדמים - Monorepo and Advanced Performance

בשיעור זה נלמד על שני נושאים מתקדמים: ניהול מונוריפו עם Turborepo, וטכניקות ביצועים מתקדמות כמו Web Workers, Virtual Scrolling ואסטרטגיות טעינה חכמות.


מונוריפו - Monorepo

מונוריפו הוא repository יחיד שמכיל מספר פרויקטים (packages) קשורים.

למה מונוריפו?

  • שיתוף קוד - קומפוננטות, utilities, types משותפים בין פרויקטים
  • ניהול תלויות - גרסה אחת של כל ספרייה בכל הפרויקטים
  • הCI/CD מאוחד - pipeline אחד לכל הפרויקטים
  • הAtomic Changes - שינוי שמשפיע על מספר פרויקטים בקומיט אחד
  • סטנדרטיזציה - הגדרות ESLint, TypeScript, Prettier משותפות

מבנה טיפוסי

my-monorepo/
  apps/
    web/           # אפליקציית Next.js
    mobile/        # אפליקציית React Native
    admin/         # פאנל ניהול
  packages/
    ui/            # ספריית קומפוננטות
    utils/         # פונקציות שירות
    config/        # הגדרות ESLint, TS, Prettier
    types/         # טיפוסי TypeScript משותפים
  turbo.json
  package.json

Turborepo

Turborepo הוא כלי build ל-monorepos שמאיץ משמעותית את זמני הבנייה.

# יצירת monorepo חדש
npx create-turbo@latest my-monorepo

# או הוספה לפרויקט קיים
npm install turbo --save-dev

הגדרת turbo.json

// turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "globalDependencies": ["**/.env.*local"],
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "!.next/cache/**", "dist/**"]
    },
    "lint": {
      "dependsOn": ["^build"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "test": {
      "dependsOn": ["build"],
      "inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts"]
    },
    "type-check": {
      "dependsOn": ["^build"]
    }
  }
}
  • dependsOn - מגדיר תלויות. ^build אומר: בנה קודם את כל ה-packages שאני תלוי בהם
  • outputs - קבצי פלט לשמירה ב-cache
  • inputs - קבצים שכשהם משתנים, ה-cache לא תקף
  • cache - האם לשמור ב-cache (false לפיתוח)
  • persistent - האם ה-task רץ לנצח (כמו dev server)

package.json הראשי

// package.json (root)
{
  "name": "my-monorepo",
  "private": true,
  "workspaces": ["apps/*", "packages/*"],
  "scripts": {
    "build": "turbo run build",
    "dev": "turbo run dev",
    "lint": "turbo run lint",
    "test": "turbo run test",
    "type-check": "turbo run type-check"
  },
  "devDependencies": {
    "turbo": "^2.0.0"
  }
}

חבילות משותפות - Shared Packages

חבילת UI

// packages/ui/package.json
{
  "name": "@myapp/ui",
  "version": "0.1.0",
  "main": "./src/index.ts",
  "types": "./src/index.ts",
  "scripts": {
    "build": "tsc",
    "lint": "eslint src/",
    "type-check": "tsc --noEmit"
  },
  "peerDependencies": {
    "react": "^18.0.0",
    "react-dom": "^18.0.0"
  }
}
// packages/ui/src/index.ts
export { Button } from './Button';
export { Input } from './Input';
export { Card } from './Card';
export type { ButtonProps, InputProps, CardProps } from './types';
// packages/ui/src/Button.tsx
import type { ButtonProps } from './types';

export function Button({ children, variant = 'primary', size = 'md', ...props }: ButtonProps) {
  return (
    <button
      className={`btn btn-${variant} btn-${size}`}
      {...props}
    >
      {children}
    </button>
  );
}

שימוש מ-app

// apps/web/package.json
{
  "name": "web",
  "dependencies": {
    "@myapp/ui": "workspace:*",
    "@myapp/utils": "workspace:*"
  }
}
// apps/web/src/app/page.tsx
import { Button, Card } from '@myapp/ui';
import { formatDate } from '@myapp/utils';

export default function Home() {
  return (
    <Card>
      <h1>דף הבית</h1>
      <p>{formatDate(new Date())}</p>
      <Button variant="primary">לחץ כאן</Button>
    </Card>
  );
}

הגדרות משותפות

// packages/config/eslint/index.js
module.exports = {
  extends: ['next/core-web-vitals', 'prettier'],
  rules: {
    'no-console': 'warn',
    'prefer-const': 'error',
  },
};
// packages/config/tsconfig/base.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "react-jsx"
  }
}

Web Workers - עובדי רקע

Web Workers מאפשרים להריץ JavaScript בחוט (thread) נפרד, מה שמונע חסימה של ה-UI.

מתי להשתמש?

  • חישובים כבדים (עיבוד תמונות, הצפנה)
  • פענוח JSON גדול
  • מיון וסינון מערכי נתונים גדולים
  • חישובים מתמטיים
// workers/sort.worker.ts
self.addEventListener('message', (event) => {
  const { data, sortBy, order } = event.data;

  const sorted = [...data].sort((a, b) => {
    const valueA = a[sortBy];
    const valueB = b[sortBy];

    if (order === 'asc') {
      return valueA > valueB ? 1 : -1;
    }
    return valueA < valueB ? 1 : -1;
  });

  self.postMessage(sorted);
});
// hooks/useWorker.ts
import { useEffect, useRef, useState, useCallback } from 'react';

function useWorker<TInput, TOutput>(workerFactory: () => Worker) {
  const workerRef = useRef<Worker | null>(null);
  const [result, setResult] = useState<TOutput | null>(null);
  const [isProcessing, setIsProcessing] = useState(false);

  useEffect(() => {
    const worker = workerFactory();

    worker.addEventListener('message', (event) => {
      setResult(event.data as TOutput);
      setIsProcessing(false);
    });

    worker.addEventListener('error', (error) => {
      console.error('Worker error:', error);
      setIsProcessing(false);
    });

    workerRef.current = worker;

    return () => worker.terminate();
  }, [workerFactory]);

  const postMessage = useCallback((data: TInput) => {
    setIsProcessing(true);
    workerRef.current?.postMessage(data);
  }, []);

  return { result, isProcessing, postMessage };
}

// שימוש
function DataTable({ data }: { data: Record<string, unknown>[] }) {
  const { result, isProcessing, postMessage } = useWorker<
    { data: Record<string, unknown>[]; sortBy: string; order: string },
    Record<string, unknown>[]
  >(() => new Worker(new URL('../workers/sort.worker.ts', import.meta.url)));

  const handleSort = (column: string) => {
    postMessage({ data, sortBy: column, order: 'asc' });
  };

  const displayData = result || data;

  return (
    <div>
      {isProcessing && <p>ממיין...</p>}
      <table>
        <thead>
          <tr>
            <th onClick={() => handleSort('name')}>שם</th>
            <th onClick={() => handleSort('age')}>גיל</th>
          </tr>
        </thead>
        <tbody>
          {displayData.map((row, i) => (
            <tr key={i}>
              <td>{row.name as string}</td>
              <td>{row.age as number}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

Intersection Observer - טעינה עצלנית

Intersection Observer מאפשר לזהות מתי אלמנט נכנס לאזור הנראה של המסך.

import { useEffect, useRef, useState } from 'react';

function useIntersectionObserver(options?: IntersectionObserverInit) {
  const [isVisible, setIsVisible] = useState(false);
  const [hasBeenVisible, setHasBeenVisible] = useState(false);
  const elementRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const element = elementRef.current;
    if (!element) return;

    const observer = new IntersectionObserver(([entry]) => {
      setIsVisible(entry.isIntersecting);
      if (entry.isIntersecting) {
        setHasBeenVisible(true);
      }
    }, options);

    observer.observe(element);

    return () => observer.disconnect();
  }, [options]);

  return { elementRef, isVisible, hasBeenVisible };
}

// טעינה עצלנית של קומפוננטות
function LazySection({ children }: { children: React.ReactNode }) {
  const { elementRef, hasBeenVisible } = useIntersectionObserver({
    rootMargin: '200px', // טעינה 200px לפני שמגיעים
    threshold: 0,
  });

  return (
    <div ref={elementRef}>
      {hasBeenVisible ? children : <div style={{ height: '400px' }} />}
    </div>
  );
}

// טעינה אינסופית - Infinite Scroll
function InfiniteList({ fetchMore }: { fetchMore: () => void }) {
  const { elementRef, isVisible } = useIntersectionObserver({
    threshold: 0.5,
  });

  useEffect(() => {
    if (isVisible) {
      fetchMore();
    }
  }, [isVisible, fetchMore]);

  return <div ref={elementRef} style={{ height: '20px' }} />;
}

גלילה וירטואלית - Virtual Scrolling

גלילה וירטואלית מציגה רק את הפריטים הנראים על המסך, גם אם יש אלפי פריטים ברשימה.

npm install @tanstack/react-virtual
import { useVirtualizer } from '@tanstack/react-virtual';
import { useRef } from 'react';

interface VirtualListProps {
  items: { id: string; name: string; email: string }[];
}

function VirtualList({ items }: VirtualListProps) {
  const parentRef = useRef<HTMLDivElement>(null);

  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 60, // גובה משוער של כל פריט
    overscan: 5, // מספר פריטים נוספים לרנדר מחוץ למסך
  });

  return (
    <div
      ref={parentRef}
      style={{ height: '500px', overflow: 'auto' }}
    >
      <div
        style={{
          height: `${virtualizer.getTotalSize()}px`,
          width: '100%',
          position: 'relative',
        }}
      >
        {virtualizer.getVirtualItems().map((virtualItem) => {
          const item = items[virtualItem.index];
          return (
            <div
              key={item.id}
              style={{
                position: 'absolute',
                top: 0,
                left: 0,
                width: '100%',
                height: `${virtualItem.size}px`,
                transform: `translateY(${virtualItem.start}px)`,
                padding: '12px 16px',
                borderBottom: '1px solid #eee',
              }}
            >
              <strong>{item.name}</strong>
              <p>{item.email}</p>
            </div>
          );
        })}
      </div>
    </div>
  );
}

// דוגמה עם 10,000 פריטים - עדיין חלק!
function App() {
  const items = Array.from({ length: 10000 }, (_, i) => ({
    id: String(i),
    name: `משתמש ${i}`,
    email: `user${i}@example.com`,
  }));

  return <VirtualList items={items} />;
}

רשת וירטואלית - Virtual Grid

import { useVirtualizer } from '@tanstack/react-virtual';
import { useRef } from 'react';

function VirtualGrid({ items, columns = 4 }: { items: string[]; columns?: number }) {
  const parentRef = useRef<HTMLDivElement>(null);
  const rows = Math.ceil(items.length / columns);

  const rowVirtualizer = useVirtualizer({
    count: rows,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 200,
    overscan: 2,
  });

  return (
    <div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
      <div style={{ height: `${rowVirtualizer.getTotalSize()}px`, position: 'relative' }}>
        {rowVirtualizer.getVirtualItems().map((virtualRow) => (
          <div
            key={virtualRow.key}
            style={{
              position: 'absolute',
              top: 0,
              left: 0,
              width: '100%',
              height: `${virtualRow.size}px`,
              transform: `translateY(${virtualRow.start}px)`,
              display: 'grid',
              gridTemplateColumns: `repeat(${columns}, 1fr)`,
              gap: '8px',
            }}
          >
            {Array.from({ length: columns }).map((_, colIndex) => {
              const itemIndex = virtualRow.index * columns + colIndex;
              if (itemIndex >= items.length) return null;
              return (
                <div key={colIndex} style={{ padding: '16px', border: '1px solid #eee', borderRadius: '8px' }}>
                  {items[itemIndex]}
                </div>
              );
            })}
          </div>
        ))}
      </div>
    </div>
  );
}

אסטרטגיות טעינה מוקדמת - Prefetching and Preloading

Prefetching ב-Next.js

import Link from 'next/link';

// Next.js עושה prefetch אוטומטי לקישורים גלויים
<Link href="/about">אודות</Link>

// ביטול prefetch
<Link href="/heavy-page" prefetch={false}>דף כבד</Link>

// Prefetch ידני עם router
import { useRouter } from 'next/navigation';

function SearchResults() {
  const router = useRouter();

  const handleHover = (id: string) => {
    // טעינה מוקדמת של דף המוצר כשהעכבר מעל
    router.prefetch(`/product/${id}`);
  };

  return (
    <div onMouseEnter={() => handleHover('123')}>
      מוצר 123
    </div>
  );
}

Preloading של משאבים

// app/layout.tsx - טעינה מוקדמת של משאבים קריטיים
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <head>
        {/* טעינה מוקדמת של פונט */}
        <link
          rel="preload"
          href="/fonts/heebo.woff2"
          as="font"
          type="font/woff2"
          crossOrigin="anonymous"
        />

        {/* טעינה מוקדמת של תמונה קריטית */}
        <link
          rel="preload"
          href="/hero-image.webp"
          as="image"
          type="image/webp"
        />

        {/* חיבור מוקדם ל-API */}
        <link rel="preconnect" href="https://api.myapp.com" />

        {/* DNS prefetch */}
        <link rel="dns-prefetch" href="https://fonts.googleapis.com" />
      </head>
      <body>{children}</body>
    </html>
  );
}

תקציבי ביצועים - Performance Budgets

// performance-budget.json
{
  "budgets": [
    {
      "resourceType": "script",
      "budget": 300
    },
    {
      "resourceType": "stylesheet",
      "budget": 100
    },
    {
      "resourceType": "image",
      "budget": 500
    },
    {
      "resourceType": "total",
      "budget": 1000
    }
  ],
  "timings": {
    "first-contentful-paint": 1500,
    "largest-contentful-paint": 2500,
    "cumulative-layout-shift": 0.1,
    "total-blocking-time": 200
  }
}
// next.config.js - הגבלת גודל bundle
const nextConfig = {
  experimental: {
    webpackBuildWorker: true,
  },

  webpack: (config, { isServer }) => {
    if (!isServer) {
      // התראה אם bundle גדול מדי
      config.performance = {
        hints: 'warning',
        maxAssetSize: 300 * 1024, // 300KB
        maxEntrypointSize: 500 * 1024, // 500KB
      };
    }
    return config;
  },
};

module.exports = nextConfig;

סיכום

בשיעור זה למדנו על:

  • מונוריפו - מבנה, יתרונות, ושיתוף קוד בין פרויקטים
  • Turborepo - הגדרת pipeline, caching, ו-workspaces
  • חבילות משותפות - UI, utils, config, types
  • Web Workers - הרצת חישובים כבדים בחוט נפרד
  • Intersection Observer - זיהוי אלמנטים גלויים, טעינה עצלנית, infinite scroll
  • גלילה וירטואלית - TanStack Virtual לרשימות ענקיות
  • טעינה מוקדמת - prefetch, preload, preconnect
  • תקציבי ביצועים - הגדרת גבולות לגודל ומהירות

שילוב של כלים אלה מאפשר לבנות אפליקציות מורכבות שעדיין מהירות ומתחזקות בקלות.