בניית אתרים שמגיבים במהירות לקלט של משתמשים היא אחד מהאתגרים הגדולים ביותר בביצועי האינטרנט, ואחת מהמשימות שצוות Chrome עובד קשה כדי לעזור למפתחי האינטרנט לעמוד בה. רק השנה הודענו שהמדד 'מאינטראקציה ועד הצגת התגובה' (INP) יעלה מסטטוס 'ניסיוני' לסטטוס 'בהמתנה'. הוא עומד להחליף את השהיה לאחר קלט ראשוני (FID) כמדד ליבה לבדיקת חוויית המשתמש באתר במרץ 2024.
כחלק מהמאמצים המתמשכים שלנו לספק ממשקי API חדשים שיעזרו למפתחי אתרים ליצור אתרים מהירים ככל האפשר, צוות Chrome מפעיל כרגע תקופת ניסיון למקור של scheduler.yield
, החל מגרסה 115 של Chrome. scheduler.yield
הוא הצעה להוספה חדשה ל-API של מתזמן המשימות, שמאפשרת דרך קלה וטובה יותר להחזיר את השליטה לשרשור הראשי בהשוואה לשיטות שבהן נעשה שימוש באופן מסורתי.
כשמשאירים את הדרך
JavaScript משתמשת במודל 'הרצה עד להשלמה' כדי לטפל במשימות. כלומר, כשמשימה פועלת בשרשור הראשי, היא פועלת כל עוד יש צורך בה כדי להשלים אותה. בסיום המשימה, השליטה מועברת בחזרה ל-thread הראשי, שמאפשר ל-thread הראשי לעבד את המשימה הבאה בתור.
מלבד מקרים קיצוניים שבהם משימה אף פעם לא מסתיימת – למשל לולאה אינסופית – הענקת הבעלות היא היבט בלתי נמנע של הלוגיקה של תזמון המשימות ב-JavaScript. זה יקרה, רק עניין של מתי, ומומלץ לעשות זאת מוקדם יותר מאשר מאוחר יותר. אם משימות נמשכות יותר מדי זמן – יותר מ-50 אלפיות השנייה, ליתר דיוק – הן נחשבות למשימות ארוכות.
משימות ארוכות גורמות לרספונסיביות נמוכה של הדף, כי הן מעכבות את היכולת של הדפדפן להגיב לקלט של המשתמש. ככל שתדירות הפעלת המשימות הארוכות תהיה גבוהה יותר, וככל שהן יימשכו זמן רב יותר, כך גדל הסיכוי שהמשתמשים יקבלו את הרושם שהדף איטי או אפילו שבור לגמרי.
עם זאת, גם אם הקוד מפעיל משימה בדפדפן, אין צורך לחכות עד שהמשימה תסתיים לפני שהשליטה מוחזרת לשרשור הראשי. כדי לשפר את הרספונסיביות לתגובות של משתמשים בדף, אפשר להשתמש ב-yield מפורש במשימה. הפעולה הזו מפרקת את המשימה כדי שהיא תסתיים בהזדמנות הקרובה. כך משימות אחרות יכולות לקבל זמן ב-thread הראשי מוקדם יותר מאשר אם הן היו צריכות להמתין לסיום של משימות ארוכות.
כשמשתמשים בהעברת הבעלות באופן מפורש, אומרים לדפדפן "הבנתי שהעבודה שאני עומד לבצע עשויה להימשך זמן מה, ואני לא רוצה שתצטרך לבצע את כל העבודה הזו לפני שתגיב לקלט של המשתמש או למשימות אחרות שעשויות להיות חשובות גם הן". זהו כלי חשוב בכלי העבודה של המפתחים, שיכול לשפר משמעותית את חוויית המשתמש.
הבעיה בשיטות הנוכחיות להגדלת התשואה
שיטה נפוצה להפקת תוצאה כוללת שימוש ב-setTimeout
עם ערך זמן קצוב לתפוגה של 0
. הסיבה לכך היא שהקריאה החוזרת (callback) שהועברה ל-setTimeout
תעביר את העבודה שנותרה למשימה נפרדת שתתווסף לתור לביצוע מאוחר יותר. במקום להמתין שהדפדפן יסיים את העבודה בעצמו, אתם אומרים "בואו נחלק את קטע העבודה הגדול הזה לחלקים קטנים יותר".
עם זאת, להעברת השליטה באמצעות setTimeout
יש תופעת לוואי שעשויה להיות לא רצויה: העבודה שמגיעה אחרי נקודת ההעברה תועבר לחלק האחורי של תור המשימות. משימות שתזמנו לפי אינטראקציות של משתמשים עדיין יעברו לראש התור כראוי, אבל העבודה שעוד נותרה לכם לעשות אחרי שתעבירו את הבעלות עליה באופן מפורש עשויה להתעכב עוד יותר בגלל משימות אחרות ממקורות מתחרים שהוצבו בתור לפניה.
כדי לראות איך זה עובד, אפשר לנסות את הדמו הזה ב-Glitch – או להתנסות בו בגרסה המוטמעת שבהמשך. הדמו מורכב מכמה לחצנים שאפשר ללחוץ עליהם, ומתיבה שמתחתיהם שמתעדת את זמן ההפעלה של המשימות. כשמגיעים לדף, מבצעים את הפעולות הבאות:
- לוחצים על הלחצן העליון הפעלת משימות באופן תקופתי. הפעולה הזו תתזמן את הפעלת המשימות החוסמות מדי פעם. כשלוחצים על הלחצן הזה, יופיעו ביומן המשימות כמה הודעות עם הכיתוב Ran blocking task with
setInterval
. - לאחר מכן, לוחצים על הלחצן 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
הפלט הזה מדגים את ההתנהגות 'סוף תור המשימות' שמתרחשת כשמשתמשים ב-yield עם 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
בפעולה:
- נווט אל
chrome://flags
. - מפעילים את הניסוי תכונות ניסיוניות של פלטפורמת אינטרנט. יכול להיות שתצטרכו להפעיל מחדש את Chrome אחרי שתבצעו את הפעולה הזו.
- עוברים אל דף הדגמה או משתמשים בגרסה המוטמעת שלו שמופיעה מתחת לרשימה הזו.
- לוחצים על הלחצן העליון הרצת משימות באופן תקופתי.
- לסיום, לוחצים על הלחצן 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:
- אם רוצים להתנסות ב-
scheduler.yield
באופן מקומי, מקלידים את הערךchrome://flags
ומזינים אותו בסרגל הכתובות של Chrome, ובוחרים באפשרות הפעלה בתפריט הנפתח בקטע תכונות ניסיוניות של פלטפורמת האינטרנט. כך התכונהscheduler.yield
(וכל התכונות הניסיוניות האחרות) יהיו זמינות רק במכונה שלכם ב-Chrome. - אם אתם רוצים להפעיל את
scheduler.yield
למשתמשים אמיתיים ב-Chromium בגרסת מקור שגלויה לכולם, תצטרכו להירשם לגרסת המקור לניסיון שלscheduler.yield
. כך תוכלו להתנסות בבטחה בתכונות המוצעות למשך תקופה מסוימת, וצוות Chrome יקבל תובנות חשובות לגבי אופן השימוש בתכונות האלה בשטח. במדריך הזה מוסבר איך פועלות תקופות הניסיון למקורות.
האופן שבו משתמשים ב-scheduler.yield
– תוך תמיכה בדפדפנים שלא מטמיעים אותו – תלוי ביעדים שלכם. אפשר להשתמש בpolyfill הרשמי. ה-polyfill שימושי אם המצב שלכם תואם לאחד מהמקרים הבאים:
- אתם כבר משתמשים ב-
scheduler.postTask
באפליקציה שלכם כדי לתזמן משימות. - אתם רוצים להגדיר משימות ועדיפויות ייצור.
- אתם רוצים לבטל משימות או לשנות את סדר העדיפויות שלהן באמצעות הכיתה
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.