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

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

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

בתפוקה

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

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

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

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

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

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

הבעיה באסטרטגיות התפוקה הנוכחיות

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

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

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

  1. לוחצים על הלחצן העליון עם התווית הרצת משימות מדי פעם. הלחצן מאפשר תזמון של משימות חוסמות כך שיפעלו מדי פעם. כשלוחצים על הלחצן הזה, יופיעו ביומן המשימות מספר הודעות עם הכיתוב הפעלת משימת חסימה עם setInterval.
  2. לאחר מכן, לוחצים על הלחצן לולאת הרצה, שמסתיימת ב-setTimeout בכל איטרציה.

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

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

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

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

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

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

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

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

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

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

כדאי לנסות!

אם 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 שמוצעת ב-API של scheduler.postTask.

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

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