10.4 - Core Web Vitals - הרצאה
מדדי ביצועים - Core Web Vitals
בשיעור זה נלמד על מדדי הביצועים שגוגל משתמש בהם כגורמי דירוג. נבין מה כל מדד מודד, מהם הספים לציון טוב, וכיצד לשפר כל מדד.
מה זה Core Web Vitals
- סט של מדדים שגוגל הגדיר כחיוניים לחווית משתמש טובה באינטרנט
- שלושה מדדים עיקריים: LCP, INP, CLS
- נמדדים ממשתמשים אמיתיים בשטח (Field Data) ולא רק בסביבת מעבדה (Lab Data)
- משפיעים על הדירוג בתוצאות החיפוש מאז יוני 2021
LCP - Largest Contentful Paint
- מודד כמה זמן לוקח עד שהאלמנט הגדול ביותר בחלק העליון של הדף (above the fold) מוצג על המסך
- האלמנט יכול להיות: תמונה, בלוק טקסט, וידאו, או אלמנט עם background-image
ספים
| ציון |
ערך |
| טוב |
עד 2.5 שניות |
| צריך שיפור |
2.5 - 4 שניות |
| גרוע |
מעל 4 שניות |
גורמים שפוגעים ב-LCP
- שרת איטי (TTFB גבוה)
- חסימת רינדור על ידי CSS ו-JavaScript
- טעינת משאבים איטית (תמונות גדולות, פונטים)
- רינדור בצד הלקוח (CSR)
כיצד לשפר LCP
// 1. images - using next/image with priority for the hero image
import Image from 'next/image';
function HeroSection() {
return (
<section>
<Image
src="/hero-image.jpg"
alt="תמונת גיבור"
width={1200}
height={600}
priority // loads immediately, without lazy loading
sizes="100vw"
/>
<h1>כותרת ראשית</h1>
</section>
);
}
<!-- 2. Preload for critical resources -->
<head>
<!-- preload the hero image -->
<link rel="preload" as="image" href="/hero-image.jpg" />
<!-- preload font -->
<link
rel="preload"
as="font"
type="font/woff2"
href="/fonts/main.woff2"
crossorigin
/>
<!-- preload critical CSS -->
<link rel="preload" as="style" href="/critical.css" />
</head>
// 3. SSR instead of CSR - content already arrives in the HTML
// Next.js Server Component - the default
export default async function HomePage() {
const data = await fetchHeroData();
return (
<main>
<h1>{data.title}</h1>
<Image src={data.image} alt={data.title} width={1200} height={600} priority />
</main>
);
}
/* 4. inline critical CSS - avoid blocking render */
/* in Next.js this happens automatically with CSS Modules */
/* avoid @import inside CSS - it creates a chain of requests */
/* bad: */
@import url('https://fonts.googleapis.com/css2?family=Inter');
/* good: use next/font */
INP - Interaction to Next Paint
- מודד את הזמן מהרגע שהמשתמש מבצע אינטראקציה (קליק, הקשה, לחיצת מקש) עד שהדפדפן מציג עדכון ויזואלי
- החליף את FID (First Input Delay) בשנת 2024
- מודד את כל האינטראקציות במהלך ביקור בדף, לא רק את הראשונה
ספים
| ציון |
ערך |
| טוב |
עד 200 מ"ש |
| צריך שיפור |
200 - 500 מ"ש |
| גרוע |
מעל 500 מ"ש |
גורמים שפוגעים ב-INP
- JavaScript כבד שרץ על ה-main thread
- Event handlers מורכבים
- עדכוני DOM גדולים
- Layout thrashing (קריאה וכתיבה חוזרות לסגנונות)
כיצד לשפר INP
// 1. split heavy work with a scheduler
function handleClick() {
// bad - blocks the main thread
const result = heavyComputation(data);
updateUI(result);
}
// good - split with requestIdleCallback or a scheduler
function handleClickBetter() {
// immediate visual update
setLoading(true);
// heavy work off the main thread
requestIdleCallback(() => {
const result = heavyComputation(data);
setResult(result);
setLoading(false);
});
}
// 2. use startTransition for non-urgent updates
import { startTransition, useState } from 'react';
function SearchPage() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
function handleSearch(e: React.ChangeEvent<HTMLInputElement>) {
const value = e.target.value;
// urgent update - immediate input
setQuery(value);
// non-urgent update - search results
startTransition(() => {
const filtered = filterItems(value);
setResults(filtered);
});
}
return (
<div>
<input value={query} onChange={handleSearch} />
<ul>
{results.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
</div>
);
}
// 3. defer heavy event handlers with debounce
import { useDeferredValue, useState } from 'react';
function FilterableList({ items }: { items: Item[] }) {
const [filter, setFilter] = useState('');
const deferredFilter = useDeferredValue(filter);
// filtering only happens when the deferred value updates
const filteredItems = items.filter(item =>
item.name.includes(deferredFilter)
);
return (
<div>
<input
value={filter}
onChange={e => setFilter(e.target.value)}
placeholder="חיפוש..."
/>
<ul>
{filteredItems.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
}
// 4. Web Worker for heavy computations
// worker.ts
self.onmessage = (e: MessageEvent) => {
const result = heavyComputation(e.data);
self.postMessage(result);
};
// component.tsx
function DataProcessor() {
const [result, setResult] = useState(null);
function processData(data: number[]) {
const worker = new Worker(new URL('./worker.ts', import.meta.url));
worker.onmessage = (e) => {
setResult(e.data);
worker.terminate();
};
worker.postMessage(data);
}
return <button onClick={() => processData(largeDataSet)}>עבד נתונים</button>;
}
CLS - Cumulative Layout Shift
- מודד את כמות ה-layout shifts הבלתי צפויים שקורים בדף
- layout shift הוא כשאלמנט בדף זז ממקומו בלי שהמשתמש ציפה לכך
- מחושב כ: impact fraction * distance fraction
ספים
| ציון |
ערך |
| טוב |
עד 0.1 |
| צריך שיפור |
0.1 - 0.25 |
| גרוע |
מעל 0.25 |
גורמים שגורמים ל-CLS
- תמונות ללא מימדים מוגדרים
- פרסומות שנטענות בצורה דינמית
- פונטים שגורמים ל-FOUT (Flash of Unstyled Text)
- תוכן שנטען בצורה דינמית ודוחף תוכן קיים
כיצד לשפר CLS
// 1. always set width and height for images
// next/image does this automatically
<Image
src="/product.jpg"
alt="מוצר"
width={400}
height={300}
/>
// for regular images:
<img
src="/product.jpg"
alt="מוצר"
width="400"
height="300"
style={{ aspectRatio: '4/3' }}
/>
/* 2. reserve space for images with aspect-ratio */
.image-container {
aspect-ratio: 16 / 9;
width: 100%;
background-color: #f3f4f6; /* placeholder */
}
.image-container img {
width: 100%;
height: 100%;
object-fit: cover;
}
// 3. loading fonts without FOUT
// in Next.js - using next/font
import { Rubik } from 'next/font/google';
const rubik = Rubik({
subsets: ['hebrew', 'latin'],
display: 'swap', // shows text with a fallback font until the font loads
fallback: ['Arial', 'sans-serif'],
});
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="he" dir="rtl" className={rubik.className}>
<body>{children}</body>
</html>
);
}
/* 4. reserve space for dynamic content */
.ad-slot {
min-height: 250px; /* minimum height for the ad */
background-color: #f9fafb;
}
.skeleton {
/* Skeleton loading - placeholder with animation */
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 4px;
}
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
// 5. use Suspense with a correctly sized fallback
import { Suspense } from 'react';
function ProductPage() {
return (
<main>
<h1>מוצרים</h1>
<Suspense fallback={<ProductListSkeleton />}>
<ProductList />
</Suspense>
</main>
);
}
function ProductListSkeleton() {
return (
<div style={{ minHeight: '600px' }}>
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="skeleton" style={{ height: '200px', marginBottom: '16px' }} />
))}
</div>
);
}
מדדים נוספים
TTFB - Time to First Byte
- הזמן מהבקשה הראשונה לשרת עד שהבייט הראשון מגיע
- מושפע מביצועי השרת, CDN, ו-DNS lookup
- ספים: טוב עד 800 מ"ש
// improve TTFB with caching in Next.js
export const revalidate = 3600; // refresh every hour
// or specific revalidation:
export default async function Page() {
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 60 }, // cache for 60 seconds
});
return <div>{/* content */}</div>;
}
FCP - First Contentful Paint
- הזמן עד שהאלמנט הראשון (טקסט, תמונה, canvas) מוצג על המסך
- ספים: טוב עד 1.8 שניות
TBT - Total Blocking Time
- סכום הזמן שבו ה-main thread חסום (משימות מעל 50 מ"ש)
- מדד Lab בלבד - לא נמדד ממשתמשים אמיתיים
- מדד מקביל ל-INP בסביבת מעבדה
- ספים: טוב עד 200 מ"ש
מדידה עם ספריית web-vitals
// lib/webVitals.ts
import { onCLS, onINP, onLCP, onFCP, onTTFB } from 'web-vitals';
interface Metric {
name: string;
value: number;
rating: 'good' | 'needs-improvement' | 'poor';
id: string;
}
function sendToAnalytics(metric: Metric) {
// send to Google Analytics
if (typeof window.gtag === 'function') {
window.gtag('event', metric.name, {
event_category: 'Web Vitals',
value: Math.round(metric.name === 'CLS' ? metric.value * 1000 : metric.value),
event_label: metric.id,
non_interaction: true,
});
}
// or send to our own server
fetch('/api/vitals', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: metric.name,
value: metric.value,
rating: metric.rating,
page: window.location.pathname,
timestamp: Date.now(),
}),
});
}
export function reportWebVitals() {
onCLS(sendToAnalytics);
onINP(sendToAnalytics);
onLCP(sendToAnalytics);
onFCP(sendToAnalytics);
onTTFB(sendToAnalytics);
}
// app/components/WebVitalsReporter.tsx
'use client';
import { useEffect } from 'react';
import { reportWebVitals } from '@/lib/webVitals';
export function WebVitalsReporter() {
useEffect(() => {
reportWebVitals();
}, []);
return null;
}
// usage in layout.tsx
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="he" dir="rtl">
<body>
{children}
<WebVitalsReporter />
</body>
</html>
);
}
שימוש בפאנל
- פתחו DevTools (F12 או Cmd+Option+I)
- עברו ללשונית Performance
- לחצו על כפתור ההקלטה (או Cmd+E)
- בצעו פעולות בדף
- עצרו את ההקלטה
מה לחפש
- Main thread - חפשו "long tasks" (משימות מעל 50 מ"ש, מסומנות באדום)
- Layout Shift - חפשו layout shift events בציר הזמן
- LCP - מסומן בציר הזמן כנקודת ציון
- Network - בדקו משאבים שנטענים לאט
טיפים:
1. הקליטו עם CPU throttling (4x slowdown) כדי לסמלץ מכשיר חלש
2. הקליטו עם Network throttling (Slow 3G) כדי לסמלץ חיבור איטי
3. הסתכלו על Summary tab בתחתית לראות חלוקת זמן
4. השתמשו ב-Bottom-Up tab למצוא פונקציות שצורכות הכי הרבה זמן
1. פתחו DevTools
2. עברו ללשונית Lighthouse
3. בחרו קטגוריות: Performance, Accessibility, Best Practices, SEO
4. בחרו מכשיר: Mobile או Desktop
5. לחצו "Analyze page load"
6. קראו את הדוח ופעלו לפי ההמלצות
סיכום
- Core Web Vitals כוללים שלושה מדדים: LCP, INP, CLS
- LCP מודד זמן עד הצגת האלמנט הגדול ביותר - יעד: מתחת ל-2.5 שניות
- INP מודד תגובתיות לאינטראקציות - יעד: מתחת ל-200 מ"ש
- CLS מודד יציבות ויזואלית - יעד: מתחת ל-0.1
- מדדים נוספים: TTFB, FCP, TBT
- ספריית web-vitals מאפשרת מדידה בזמן אמת מהמשתמשים
- Chrome DevTools Performance Panel הוא כלי חיוני לאבחון בעיות ביצועים
- שיפור Core Web Vitals משפר גם את חווית המשתמש וגם את דירוג ה-SEO