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¶
גלילה וירטואלית מציגה רק את הפריטים הנראים על המסך, גם אם יש אלפי פריטים ברשימה.
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
- תקציבי ביצועים - הגדרת גבולות לגודל ומהירות
שילוב של כלים אלה מאפשר לבנות אפליקציות מורכבות שעדיין מהירות ומתחזקות בקלות.