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

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

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

בזמן ההמתנה

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

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

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

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

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

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

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

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

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

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

  1. לוחצים על הלחצן העליון עם התווית הפעלת משימות מעת לעת כדי לתזמן את המשימות שחוסמות את הגישה להפעלה מדי פעם. כשלוחצים על הלחצן הזה, יומן המשימות מתמלא בכמה הודעות עם הכיתוב Ran blocking task with setInterval.
  2. לאחר מכן, לוחצים על הלחצן עם התווית Run loop, yielding with setTimeout on each iteration (הפעלת לולאה, יצירת תשואה עם 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

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

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

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

כניסה אל scheduler.yield

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

חשוב לציין שהחזרת התוצאה לא הייתה מטרה בתכנון של setTimeout, אלא תוצאת לוואי נחמדה בתזמון של קריאה חוזרת להפעלה בנקודה מאוחרת יותר בעתיד – גם אם צוין ערך פסק זמן של 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. מפעילים את הניסוי Experimental Web Platform features. יכול להיות שתצטרכו להפעיל מחדש את Chrome אחרי שתעשו את זה.
  3. אפשר לעבור אל דף ההדגמה או להשתמש בגרסה המוטמעת הבאה שלו אחרי הרשימה הזו.
  4. לוחצים על הלחצן העליון עם התווית הפעלת משימות באופן תקופתי.
  5. לבסוף, לוחצים על הלחצן Run loop, yielding with scheduler.yield on each iteration (הפעלת לולאה, יצירת תשואה עם 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, אפשר לראות שהלולאה – למרות שהיא מחזירה ערך אחרי כל איטרציה – לא שולחת את העבודה שנותרה לסוף התור, אלא לתחילת התור. כך תוכלו ליהנות משני העולמות: תוכלו להשתמש ב-yield כדי לשפר את מהירות התגובה של הקלט באתר, אבל גם לוודא שהעבודה שרציתם לסיים אחרי השימוש ב-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 אם הוא זמין, אבל לא משתמשת ב-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) מ-Unsplash, מאת Jonathan Allison.