אופטימיזציה של משימות ארוכות לשיפור היענות האתר
תארו לעצמכם שמשתמש לוחץ על כפתור, והממשק קופא למשך חצי שנייה. זו משימה ארוכה (Long Task) — משימה בשרשור הראשי שנמשכת יותר מ-50 אלפיות שנייה. INP (Interaction to Next Paint) — מדד Core Web Vitals — סובל ישירות ממשימות כאלה. לפי Interaction to Next Paint (INP), עיכובים מעל 200 אלפיות שנייה יכולים להפחית המרות בעד 3%. הצוות שלנו מתמחה באיתור וביטול משימות ארוכות ב-SPA מורכבים. ב-5 שנים, אופטימזנו מעל 30 פרויקטים, והשגנו הפחתות INP מ->500 אלפיות שנייה ל-<200 אלפיות שנייה. לקוחות מדווחים על מעורבות מוגברת וירידה בשיעורי נטישה לאחר האופטימיזציה. בנוסף, הפחתת TBT (Total Blocking Time) חוסכת ישירות במשאבי שרת ומורידה עלויות תחזוקה.
מדוע משימות ארוכות חוסמות את הממשק
כל משימה ארוכה שנמשכת יותר מ-50 אלפיות שנייה חוסמת עיבוד קלט משתמש: לחיצות, גלילות, הקשות מקשים. INP לוכד את עיכוב התגובה הגרוע ביותר לאינטראקציות. ביטול משימות ארוכות הוא דרך ישירה לשיפור Core Web Vitals והגברת המרות. אנו מבטיחים לפחות 30% שיפור ב-INP לאחר האופטימיזציה.
כיצד למצוא משימות ארוכות בסביבת ייצור
פתחו את Chrome DevTools → לשונית Performance → התחילו הקלטה → דמו טעינה או אינטראקציה → עצרו. במסלול Main, משולשים אדומים מסמנים משימות שנמשכות יותר מ-50 אלפיות שנייה. לחצו על משימה — עץ הקריאות למטה מראה אילו פונקציות נקראו וכמה זמן כל אחת נמשכה. מדדים מרכזיים: Total Blocking Time (סכום של (משך - 50ms) על פני כל המשימות הארוכות), חלקים של Scripting, Rendering, Painting.
לפרופיולינג בסביבת ייצור, השתמשו ב-PerformanceObserver. הוא שולח נתוני משימות ארוכות לשרת לניתוח. הנה דוגמה:
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
navigator.sendBeacon('/api/longtasks', JSON.stringify({
duration: entry.duration,
startTime: entry.startTime,
url: location.href,
userAgent: navigator.userAgent,
}));
}
});
observer.observe({ type: 'longtask', buffered: true }); כלים למציאת משימות ארוכות
- Chrome DevTools: לשונית Performance, מסלול Main, פסים צבעוניים.
- PerformanceObserver בסביבת ייצור לאיסוף נתוני שטח.
- ספריית web-vitals למדידת INP בתנאים אמיתיים.
כיצד לבטל משימה ארוכה: סקירת שיטות
| שיטה | תמיכת דפדפנים | עיכוב תזמון מחדש | נוחות |
|---|---|---|---|
const observer = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { navigator.sendBeacon('/api/longtasks', JSON.stringify({ duration: entry.duration, startTime: entry.startTime, url: location.href, userAgent: navigator.userAgent, })); } }); observer.observe({ type: 'longtask', buffered: true }); | כל הדפדפנים | מינימום 4 אלפיות שנייה | דורש חלוקה ידנית |
setTimeout(fn, 0) | Chrome 115+, Edge 115+ | ~0 אלפיות שנייה (בעדיפות גבוהה) | וויתור מובנה |
scheduler.yield() | רוב הניידים והשולחניים | אדפטיבי | רק לא קריטי |
requestIdleCallback מספק עיכוב מינימלי ומפנה שליטה אוטומטית באירועי קלט. הוא יעיל פי 2–5 מ-setTimeout בשל תעדוף אינטראקציות.
דפוסי אופטימיזציה מעשיים
כיצד לחלק משימה עם scheduler.yield()
הדרך הישירה ביותר לבטל משימה ארוכה היא לשבור אותה לחלקים ולפנות שליטה בין החלקים. גישה מודרנית באמצעות scheduler.yield() (Chrome 115+):
async function processLargeArrayModern(items) {
const CHUNK_SIZE = 100;
for (let i = 0; i < items.length; i += CHUNK_SIZE) {
const chunk = items.slice(i, i + CHUNK_SIZE);
chunk.forEach(processItem);
if (i + CHUNK_SIZE < items.length) {
await scheduler.yield();
}
}
}פוליפיל לדפדפנים ללא scheduler.yield() משתמש ב-MessageChannel — עיכוב ~0 אלפיות שנייה.
מדוע להעביר חישובים ל-Web Workers?
חישובים כבדים שאינם עובדים עם ה-DOM (ניתוח JSON, קריפטוגרפיה, מיון מורכב) צריכים לעבור ל-Workers. דוגמת WorkerPool:
// main.js
class WorkerPool {
constructor(workerScript, poolSize = navigator.hardwareConcurrency || 4) {
this.workers = Array.from({ length: poolSize }, () => new Worker(workerScript));
this.queue = [];
this.available = [...this.workers];
}
run(type, payload) {
return new Promise((resolve, reject) => {
const task = { type, payload, resolve, reject };
if (this.available.length > 0) {
this._dispatch(task);
} else {
this.queue.push(task);
}
});
}
_dispatch({ type, payload, resolve, reject }, worker = this.available.pop()) {
worker.onmessage = ({ data }) => {
resolve(data.result);
this.available.push(worker);
if (this.queue.length > 0) this._dispatch(this.queue.shift());
};
worker.onerror = reject;
worker.postMessage({ type, payload });
}
}
const pool = new WorkerPool('/heavy-worker.js', 2);
const sortedProducts = await pool.run('SORT_PRODUCTS', products);
React: startTransition ו-useDeferredValue
React 18 מאפשר הבחנה מפורשת בין עדכונים דחופים ללא דחופים. async function processLargeArrayModern(items) { const CHUNK_SIZE = 100; for (let i = 0; i < items.length; i += CHUNK_SIZE) { const chunk = items.slice(i, i + CHUNK_SIZE); chunk.forEach(processItem); if (i + CHUNK_SIZE < items.length) { await scheduler.yield(); } } } לעדכוני מצב לא קריטיים:
import { startTransition, useState } from 'react';
function SearchPage() {
const [inputValue, setInputValue] = useState('');
const [searchQuery, setSearchQuery] = useState('');
function handleInput(e) {
const value = e.target.value;
setInputValue(value);
startTransition(() => setSearchQuery(value));
}
return (
<>
<input value={inputValue} onChange={handleInput} />
<SearchResults query={searchQuery} />
</>
);
}scheduler.yield() לרשימות: דוחה עדכוני סינון.
כיצד רינדור רשימה וירטואלית מאיץ את התצוגה?
רינדור אלפי אלמנטי DOM יוצר משימה ארוכה אחת גדולה. וירטואליזציה מרנדרת רק אלמנטים גלויים. דוגמה עם // main.js class WorkerPool { constructor(workerScript, poolSize = navigator.hardwareConcurrency || 4) { this.workers = Array.from({ length: poolSize }, () => new Worker(workerScript)); this.queue = []; this.available = [...this.workers]; } run(type, payload) { return new Promise((resolve, reject) => { const task = { type, payload, resolve, reject }; if (this.available.length > 0) { this._dispatch(task); } else { this.queue.push(task); } }); } _dispatch({ type, payload, resolve, reject }, worker = this.available.pop()) { worker.onmessage = ({ data }) => { resolve(data.result); this.available.push(worker); if (this.queue.length > 0) this._dispatch(this.queue.shift()); }; worker.onerror = reject; worker.postMessage({ type, payload }); } } const pool = new WorkerPool('/heavy-worker.js', 2); const sortedProducts = await pool.run('SORT_PRODUCTS', products); :
import { useVirtualizer } from '@tanstack/react-virtual';
function VirtualList({ items }) {
const parentRef = useRef(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 72,
overscan: 5,
});
return (
<div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
<div style={{ height: virtualizer.getTotalSize() }}>
{virtualizer.getVirtualItems().map(virtualItem => (
<div
key={virtualItem.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${virtualItem.start}px)`,
}}
>
<ListItem item={items[virtualItem.index]} />
</div>
))}
</div>
</div>
);
} מדידת תוצאות
לפני ואחרי האופטימיזציה, מדדו בתנאי מעבדה (Lighthouse, 4× CPU throttle, 3G) ובשטח (INP באמצעות startTransition). תוצאות אופייניות:
| מדד | לפני אופטימיזציה | אחרי אופטימיזציה |
|---|---|---|
| TBT | 1–3 שניות | <300 אלפיות שנייה |
| INP | 400–600 אלפיות שנייה | <200 אלפיות שנייה |
לפחות 30% שיפור ב-INP — מאומת מניסיון על פני 30+ פרויקטים.
מה כוללת העבודה ולוחות זמנים
- ביקורת פרופיולינג: איסוף משימות ארוכות דרך DevTools וניטור ייצור, ניתוח מחסנית קריאות. (2–3 ימים)
- פיצול קוד וחלוקה: פיצול בלוקים גדולים, טעינה עצלה של מודולים.
- העברת חישובים כבדים ל-Web Workers עם WorkerPool.
- וירטואליזציה של רשימות ארוכות באמצעות react-virtual או דומה.
- אופטימיזציה של רכיבי React: startTransition, useDeferredValue, memoization.
- מדידה חוזרת ודוח: השוואת INP/TBT לפני ואחרי, המלצות.
מחזור אופטימיזציה מלא ל-SPA מורכב — 2–4 שבועות. לאתרים פשוטים יותר עם רינדור צד שרת — 3–7 ימים.
כמה זמן נמשכת האופטימיזציה?
אבחון: 2–3 ימים; מחזור מלא: 2–4 שבועות ל-SPA, 3–7 ימים לאתרים פשוטים יותר. העלות מחושבת באופן אישי.
בקשו ביקורת משימות ארוכות — קבלו דוח עם המלצות ותוכנית. צרו קשר כדי להתחיל לשפר את היענות האתר שלכם היום.







