חדש: גרסת המקור לניסיון של scheduler.yield

בניית אתרים שמגיבים במהירות לקלט של משתמשים היא אחד מהאתגרים הגדולים ביותר בביצועי האינטרנט, ואחת מהמשימות שצוות Chrome עובד קשה כדי לעזור למפתחי אתרים לעמוד בה. רק השנה הודענו שהמדד 'מאינטראקציה ועד הצגת התגובה' (INP) יעלה מסטטוס 'ניסיוני' לסטטוס 'בהמתנה'. הוא עומד להחליף את השהיה לאחר קלט ראשוני (FID) כמדד ליבה לבדיקת חוויית המשתמש באתר במרץ 2024.

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

כשמשאירים את הדרך

JavaScript משתמשת במודל 'הרצה עד להשלמה' כדי לטפל במשימות. כלומר, כשמשימה פועלת בשרשור הראשי, היא פועלת כל עוד יש צורך בה כדי להשלים אותה. בסיום המשימה, השליטה מועברת בחזרה ל-thread הראשי, שמאפשר ל-thread הראשי לעבד את המשימה הבאה בתור.

מלבד מקרים קיצוניים שבהם משימה אף פעם לא מסתיימת – למשל לולאה אינסופית – הענקת הבעלות היא היבט בלתי נמנע של הלוגיקה של תזמון המשימות ב-JavaScript. זה יקרה, רק עניין של מתי, ומומלץ לעשות זאת מוקדם יותר מאשר מאוחר יותר. אם משימות נמשכות יותר מדי זמן – יותר מ-50 אלפיות השנייה, ליתר דיוק – הן נחשבות למשימות ארוכות.

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

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

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

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

הבעיה בשיטות הנוכחיות להגדלת התשואה

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

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

כדי לראות איך זה עובד, אפשר לנסות את הדמו הזה ב-Glitch – או להתנסות בו בגרסה המוטמעת שבהמשך. הדמו מורכב מכמה לחצנים שאפשר ללחוץ עליהם, ומתיבה שמתחתיהם שמתעדת את זמן ההפעלה של המשימות. כשמגיעים לדף, מבצעים את הפעולות הבאות:

  1. לוחצים על הלחצן העליון הפעלת משימות באופן תקופתי, כדי לתזמן את הפעלת המשימות החוסמות מדי פעם. כשלוחצים על הלחצן הזה, יופיעו ביומן המשימות כמה הודעות עם הכיתוב Ran blocking task with setInterval.
  2. לאחר מכן, לוחצים על הלחצן Run loop, yielding with setTimeout on each iteration.

בתיבה שבתחתית ההדגמה יופיע משהו כזה:

Processing loop item 1
Processing loop item 2
Ran blocking task via setInterval
Processing loop item 3
Ran blocking task via setInterval
Processing loop item 4
Ran blocking task via setInterval
Processing loop item 5
Ran blocking task via setInterval
Ran blocking task via setInterval

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

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

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

יש להיכנס אל scheduler.yield

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

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

scheduler.yield היא פונקציה שמעבירה את השליטה לשרשור הראשי ומחזירה Promise כשמתבצעת קריאה אליה. כלומר, אפשר await אותו בפונקציה async:

async function yieldy () {
  // Do some work...
  // ...

  // Yield!
  await scheduler.yield();

  // Do some more work...
  // ...
}

כדי לראות את scheduler.yield בפעולה:

  1. נווט אל chrome://flags.
  2. מפעילים את הניסוי תכונות ניסיוניות של פלטפורמת אינטרנט. יכול להיות שתצטרכו להפעיל מחדש את Chrome אחרי שתבצעו את הפעולה הזו.
  3. עוברים אל דף הדגמה או משתמשים בגרסה המוטמעת שלו שמופיעה מתחת לרשימה הזו.
  4. לוחצים על הלחצן העליון הפעלת משימות באופן תקופתי.
  5. לסיום, לוחצים על הלחצן Run loop, yielding with scheduler.yield on each iteration.

הפלט בתיבה שבתחתית הדף אמור להיראות כך:

Processing loop item 1
Processing loop item 2
Processing loop item 3
Processing loop item 4
Processing loop item 5
Ran blocking task via setInterval
Ran blocking task via setInterval
Ran blocking task via setInterval
Ran blocking task via setInterval
Ran blocking task via setInterval

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

כדאי לנסות!

אם scheduler.yield מעניין אתכם ואתם רוצים לנסות אותו, תוכלו לעשות זאת בשתי דרכים החל מגרסה 115 של Chrome:

  1. אם רוצים להתנסות ב-scheduler.yield באופן מקומי, מקלידים את הערך chrome://flags ומזינים אותו בסרגל הכתובות של Chrome, ובוחרים באפשרות הפעלה בתפריט הנפתח בקטע תכונות ניסיוניות של פלטפורמת האינטרנט. כך התכונה scheduler.yield (וכל התכונות הניסיוניות האחרות) יהיו זמינות רק במכונה שלכם ב-Chrome.
  2. אם אתם רוצים להפעיל את scheduler.yield למשתמשים אמיתיים ב-Chromium בגרסת מקור שגלויה לכולם, תצטרכו להירשם לגרסת המקור לניסיון של scheduler.yield. כך תוכלו להתנסות בבטחה בתכונות המוצעות למשך תקופה מסוימת, וצוות Chrome יקבל תובנות חשובות לגבי האופן שבו התכונות האלה משמשות בשטח. במדריך הזה מוסבר איך פועלות תקופות הניסיון למקורות.

האופן שבו משתמשים ב-scheduler.yield – תוך תמיכה בדפדפנים שלא מטמיעים אותו – תלוי ביעדים שלכם. אפשר להשתמש בpolyfill הרשמי. ה-polyfill שימושי אם המצב שלכם תואם לתנאים הבאים:

  1. אתם כבר משתמשים ב-scheduler.postTask באפליקציה שלכם כדי לתזמן משימות.
  2. אתם רוצים להגדיר משימות ועדיפויות ייצור.
  3. אתם רוצים לבטל משימות או לשנות את סדר העדיפויות שלהן באמצעות הכיתה TaskController ש-scheduler.postTask API מציע.

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

// A function for shimming scheduler.yield and setTimeout:
function yieldToMain () {
  // Use scheduler.yield if it exists:
  if ('scheduler' in window && 'yield' in scheduler) {
    return scheduler.yield();
  }

  // Fall back to setTimeout:
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  });
}

// Example usage:
async function doWork () {
  // Do some work:
  // ...

  await yieldToMain();

  // Do some other work:
  // ...
}

אפשר להשתמש באפשרות הזו, אבל כפי שאפשר לנחש, דפדפנים שלא תומכים ב-scheduler.yield יחזירו נתונים בלי התנהגות של 'ראש התור'. אם אתם מעדיפים לא להשתמש בכלל ב-yield, תוכלו לנסות גישה אחרת שבה נעשה שימוש ב-scheduler.yield אם הוא זמין, אבל לא נעשה שימוש בכלל אם הוא לא זמין:

// A function for shimming scheduler.yield with no fallback:
function yieldToMain () {
  // Use scheduler.yield if it exists:
  if ('scheduler' in window && 'yield' in scheduler) {
    return scheduler.yield();
  }

  // Fall back to nothing:
  return;
}

// Example usage:
async function doWork () {
  // Do some work:
  // ...

  await yieldToMain();

  // Do some other work:
  // ...
}

scheduler.yield הוא תוספת מעניינת ל-Scheduler API, שבעזרתה המפתחים יוכלו לשפר את היענות המערכת בקלות רבה יותר מאשר באמצעות אסטרטגיות ה-Yielding הנוכחיות. אם scheduler.yield נראה לכם ממשק API שימושי, תוכלו להשתתף במחקר שלנו כדי לעזור לנו לשפר אותו, ולשלוח משוב על דרכים לשיפור נוסף.

התמונה הראשית (Hero) מ-Unsplash, מאת Jonathan Allison.