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

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

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

בתפוקה

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

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

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

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

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

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

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

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

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

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

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

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