שימוש ב-scheduler.yield() כדי לפצל משימות ארוכות

תאריך פרסום: 6 במרץ 2025

Browser Support

  • Chrome: 129.
  • Edge: 129.
  • Firefox: not supported.
  • Safari: not supported.

Source

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

scheduler.yield() היא דרך להעביר את העדיפות לשרשור הראשי – ומאפשרת לדפדפן להריץ משימות בהמתנה בעדיפות גבוהה – ולאחר מכן להמשיך את הביצועים מהמקום שבו הם הופסקו. כך הדף יהיה תגובה יותר מהר, וכתוצאה מכך יעזור לשפר את הזמן מאינטראקציה ועד לציור הבא (INP).

scheduler.yield מציע ממשק API ארגונומי שעושה בדיוק את מה שהוא אומר: ביצוע הפונקציה שבה היא נקראת מושהה בביטוי await scheduler.yield() ומעביר את הבעלות ל-thread הראשי, ומפרק את המשימה. ביצוע שאר הפונקציה – שנקרא המשך הפונקציה – יהיה מתוזמן לפעול במשימה חדשה של לולאת אירועים.

async function respondToUserClick() {
  giveImmediateFeedback();
  await scheduler.yield(); // Yield to the main thread.
  slowerComputation();
}

היתרון הספציפי של scheduler.yield הוא שהמשך לאחר ה-yield מתוזמן לפעול לפני הפעלת משימות דומות אחרות שהדף הכניס לתור. המערכת נותנת עדיפות להמשך משימות על פני התחלת משימות חדשות.

אפשר גם להשתמש בפונקציות כמו setTimeout או scheduler.postTask כדי לפצל משימות, אבל הפונקציות האלה מריצות את ההמשכות בדרך כלל אחרי משימות חדשות שכבר נמצאות בתור, וכך עלולות לגרום לעיכובים ארוכים בין העברת השליטה לשרשור הראשי לבין השלמת העבודה.

המשכות לפי תעדוף אחרי הענקת הבעלות

scheduler.yield הוא חלק מ-Prioritized Task Scheduling API. כמפתחי אינטרנט, אנחנו בדרך כלל לא מדברים על הסדר שבו לולאת האירועים מפעילה משימות במונחים של סדר עדיפויות מפורש, אבל סדר העדיפויות היחסי תמיד קיים, למשל קריאה חוזרת (callback) של requestIdleCallback שתופעל אחרי כל קריאה חוזרת של setTimeout שנמצאת בתור, או מאזין לאירועי קלט שמופעל בדרך כלל לפני משימה שנמצאת בתור עם setTimeout(callback, 0).

התכונה 'תזמון משימות לפי תעדוף' רק ממחישה את זה בצורה ברורה יותר, ומאפשרת לכם להבין בקלות איזו משימה תרוץ לפני אחרת, וגם לשנות את סדר ההפעלה שלהן לפי הצורך.

כפי שצוין, העדיפות של המשך הביצוע של פונקציה אחרי הענקת הבעלות באמצעות scheduler.yield() גבוהה יותר מזו של הפעלת משימות אחרות. הרעיון המנחה הוא שהמשך המשימה צריך לפעול קודם, לפני שממשיכים למשימות אחרות. אם המשימה היא קוד שמתנהג בצורה תקינה ומעביר מדי פעם את השליטה כדי שהדפדפן יוכל לבצע דברים חשובים אחרים (כמו תגובה לקלט של משתמשים), לא צריך להעניש אותו על העברת השליטה על ידי מתן עדיפות נמוכה יותר למשימה בהשוואה למשימות דומות אחרות.

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

setTimeout(myJob);
setTimeout(someoneElsesJob);

במקרה הזה, שתי הקריאות ל-setTimeout נמצאות זו לצד זו, אבל בדף אמיתי הן יכולות להופיע במקומות שונים לגמרי, למשל סקריפט של צד ראשון וסקריפט של צד שלישי שמגדירים בנפרד משימות לצורך הפעלה, או שתי משימות מרכיבים נפרדים שמופעלות עמוק בתוך מתזמן המשימות של המסגרת.

כך זה עשוי להיראות בכלי הפיתוח:

שתי משימות שמוצגות בחלונית הביצועים של Chrome DevTools. שתי המשימות הן משימות ארוכות, והפונקציה myJob חולפת על כל ביצוע המשימה הראשונה, והפונקציה someoneElsesJob חולפת על כל ביצוע המשימה השנייה.

הפונקציה myJob מסומנת כמשימה ארוכה, שחוסמת את הדפדפן מלעשות שום דבר אחר בזמן שהיא פועלת. בהנחה שהבקשה מגיעה מסקריפט של צד ראשון, אפשר לפרק אותה:

function myJob() {
  // Run part 1.
  myJobPart1();
  // Yield with setTimeout() to break up long task, then run part2.
  setTimeout(myJobPart2, 0);
}

מכיוון ש-myJobPart2 תוזמן לפעול עם setTimeout בתוך myJob, אבל התזמון הזה פועל אחרי ש-someoneElsesJob כבר תוזמן, כך ייראה הביצוע:

שלוש משימות שמוצגות בחלונית הביצועים של כלי הפיתוח ל-Chrome. המשימה הראשונה מפעילה את הפונקציה 'myJobPart1', המשימה השנייה היא משימה ארוכה שמפעילה את 'someoneElsesJob', ולבסוף המשימה השלישית מפעילה את 'myJobPart2'.

פיצלנו את המשימה באמצעות setTimeout כדי שהדפדפן יוכל להגיב במהלך ביצוע myJob, אבל עכשיו החלק השני של myJob פועל רק אחרי ש-someoneElsesJob מסתיים.

במקרים מסוימים, זה בסדר, אבל בדרך כלל זו לא דרך אופטימלית. myJob וויתר על שרשור ה-UI הראשי כדי לוודא שהדף יוכל להגיב לקלט של המשתמשים, לא כדי לוותר על שרשור ה-UI הראשי לגמרי. במקרים שבהם someoneElsesJob איטי במיוחד, או שהתזמנו הרבה משימות אחרות מלבד someoneElsesJob, יכול להיות שיעבור זמן רב עד שהמחצית השנייה של myJob תופעל. סביר להניח שהמטרה של המפתח לא הייתה זו כשהוסיף את setTimeout ל-myJob.

מזינים scheduler.yield(), וכך ההמשך של כל פונקציה שמפעילה אותה מופיע בתור בעדיפות מעט גבוהה יותר מאשר התחלת משימות דומות אחרות. אם משנים את myJob כך שישתמש בו:

async function myJob() {
  // Run part 1.
  myJobPart1();
  // Yield with scheduler.yield() to break up long task, then run part2.
  await scheduler.yield();
  myJobPart2();
}

עכשיו הביצוע נראה כך:

שתי משימות שמוצגות בחלונית הביצועים של Chrome DevTools. שתי המשימות הן משימות ארוכות, והפונקציה myJob חולפת על כל ביצוע המשימה הראשונה, והפונקציה someoneElsesJob חולפת על כל ביצוע המשימה השנייה.

הדפדפן עדיין יכול להגיב במהירות, אבל עכשיו העדיפות היא להמשיך את המשימה myJob ולא להתחיל את המשימה החדשה someoneElsesJob, כך ש-myJob תושלם לפני ש-someoneElsesJob תתחיל. המצב הזה קרוב הרבה יותר לציפייה של הענקת עדיפות ל-thread הראשי כדי לשמור על תגובה מיידית, ולא ויתור מוחלט על ה-thread הראשי.

ירושה של עדיפות

כחלק מ-Prioritized Task Scheduling API, ה-API scheduler.yield() משתלב היטב עם העדיפויות המפורשות שזמינות ב-scheduler.postTask(). בלי להגדיר עדיפות באופן מפורש, קריאה חוזרת (callback) של scheduler.yield() בתוך scheduler.postTask() תפעל באופן דומה לדוגמה הקודמת.

עם זאת, אם מגדירים עדיפות, למשל עדיפות 'background' נמוכה:

async function lowPriorityJob() {
  part1();
  await scheduler.yield();
  part2();
}

scheduler.postTask(lowPriorityJob, {priority: 'background'});

המשך העבודה יתבצע עם עדיפות גבוהה יותר ממשימות 'background' אחרות – המשך העבודה עם העדיפות הצפויה יתבצע לפני כל עבודה 'background' בהמתנה – אבל עדיין עם עדיפות נמוכה יותר ממשימות אחרות שמוגדרות כברירת מחדל או עם עדיפות גבוהה. היא תישאר כעבודה 'background'.

כלומר, אם תזמנו עבודה בעדיפות נמוכה באמצעות 'background' scheduler.postTask() (או באמצעות requestIdleCallback), ההמשך אחרי scheduler.yield() בתוכה ימתין גם עד שרוב המשימות האחרות יסתיימו וה-thread הראשי יהיה פנוי לפעולה. זה בדיוק מה שרוצים כשמשתמשים ב-yield במשימה בעדיפות נמוכה.

איך משתמשים ב-API?

בשלב הזה, scheduler.yield() זמין רק בדפדפנים המבוססים על Chromium, כך שצריך לזהות את התכונה ולהשתמש בדרך חלופית להעברת בקשות לדפדפנים אחרים.

scheduler-polyfill הוא polyfill קטן ל-scheduler.postTask ול-scheduler.yield שמשתמש באופן פנימי בשילוב של שיטות כדי לדמות הרבה מהיכולות של ממשקי ה-API לתזמון בדפדפנים אחרים (אבל אין תמיכה בירושה של תעדוף scheduler.yield()).

אם אתם רוצים להימנע משימוש ב-polyfill, אחת מהשיטות היא להשתמש ב-yield באמצעות setTimeout() ולהשלים עם אובדן ההמשך בעדיפות גבוהה, או אפילו לא להשתמש ב-yield בדפדפנים שלא תומכים ב-yield, אם זה לא מקובל עליכם. מידע נוסף זמין במסמכי העזרה של scheduler.yield() בנושא אופטימיזציה של משימות ארוכות.

אפשר להשתמש בסוגים של wicg-task-scheduling גם כדי לקבל בדיקת סוגים ותמיכה ב-IDE אם אתם מזוהים תכונות scheduler.yield() ומוסיפים חלופה חלופית בעצמכם.

מידע נוסף

למידע נוסף על ה-API ועל האינטראקציה שלו עם תעדוף המשימות ו-scheduler.postTask(), אפשר לעיין במסמכים scheduler.yield() ו-תזמון משימות לפי תעדוף ב-MDN.

מידע נוסף על משימות ארוכות, על ההשפעה שלהן על חוויית המשתמש ועל הפעולות שאפשר לבצע לגביהן זמין במאמר אופטימיזציה של משימות ארוכות.