בניית אתרים המגיבים במהירות לקלט של משתמשים היא אחד ההיבטים המאתגרים ביותר של ביצועי אינטרנט - אתר שצוות 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 הראשי מהר יותר מאשר אם הן היו צריכות להמתין עד שמשימות ארוכות יסתיימו.
כשאתם מוותרים באופן מפורש, אתם אומרים לדפדפן "היי, אני מבין שהעבודה שאני עומד לעשות עשויה להימשך זמן מה, ואני לא רוצה שתצטרכו לבצע את כל העבודה הזאת לפני שאתם מגיבים לקלט של משתמשים או למשימות אחרות שעשויות להיות חשובות גם כן". זהו כלי חשוב בארגז הכלים של המפתחים, שיכול לתרום מאוד לשיפור חוויית המשתמש.
הבעיה באסטרטגיות התפוקה הנוכחיות
שיטה נפוצה לניצול משתמשת ב-setTimeout
עם ערך פסק זמן של 0
. התכונה הזו פועלת כי הקריאה החוזרת (callback) שהועברה אל setTimeout
תעביר את העבודה הנותרת למשימה נפרדת שתמתין בתור להפעלה הבאה. במקום להמתין עד שהדפדפן יצבור נתונים מעצמו, אתם אומרים "בואו נחלק את העבודה הגדולה הזו לחלקים קטנים יותר".
עם זאת, לתפוקה באמצעות setTimeout
יש תופעת לוואי שעלולה להיות לא רצויה: העבודה שמגיעה אחרי נקודת התפוקה תעבור לסוף תור המשימות. משימות שתוזמנו על ידי אינטראקציות של משתמשים עדיין יופיעו בתחילת 'הבאים בתור'. עם זאת, שאר העבודה שרציתם לבצע אחרי שהשלמתם אותן בצורה מפורשת עלולה להתעכב עוד יותר על ידי משימות אחרות ממקורות מתחרים שעמדו לתור לפני כן.
כדי לראות את זה בפעולה, אפשר לנסות את ההדגמה הזו של תקלה – או להתנסות בה בגרסה המוטמעת שלמטה. ההדגמה מורכבת מכמה לחצנים שניתן ללחוץ עליהם, ומתחתם תיבה שמתעדת את הרצת המשימות. כשמגיעים לדף, מבצעים את הפעולות הבאות:
- לוחצים על הלחצן העליון עם התווית הרצת משימות מדי פעם. הלחצן מאפשר תזמון של משימות חוסמות כך שיפעלו מדי פעם. כשלוחצים על הלחצן הזה, יופיעו ביומן המשימות מספר הודעות עם הכיתוב הפעלת משימת חסימה עם
setInterval
. - לאחר מכן, לוחצים על הלחצן לולאת הרצה, שמסתיימת ב-
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
בפעולה, צריך לבצע את הפעולות הבאות:
- נווט אל
chrome://flags
. - מפעילים את הניסוי תכונות ניסיוניות של פלטפורמת האינטרנט. ייתכן שתצטרכו להפעיל מחדש את Chrome לאחר מכן.
- אפשר לעבור אל דף ההדגמה או להשתמש בגרסה המוטמעת שלו שמתחת לרשימה הזו.
- לוחצים על הלחצן העליון עם התווית הרצת משימות מדי פעם.
- לסיום, לוחצים על הלחצן לולאת הרצה, שיוצאת עם
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:
- אם רוצים להתנסות ב-
scheduler.yield
באופן מקומי, מקלידיםchrome://flags
בסרגל הכתובות של Chrome ובוחרים באפשרות הפעלה בתפריט הנפתח שבקטע תכונות ניסיוניות של פלטפורמת האינטרנט. כךscheduler.yield
(וכל תכונה ניסיונית אחרת) תהיה זמינה רק במופע של Chrome. - אם ברצונך להפעיל את
scheduler.yield
למשתמשי Chromium אמיתיים ממקור נגיש באופן ציבורי, עליך להירשם לגרסת המקור לניסיון שלscheduler.yield
. כך תוכלו להתנסות בבטחה עם תכונות מוצעות למשך פרק זמן נתון, ולצוות Chrome יש תובנות חשובות לגבי אופן השימוש בתכונות האלה בתחום. למידע נוסף על האופן שבו פועלות גרסאות המקור לניסיון, ניתן לקרוא את המדריך הזה.
אופן השימוש שלך ב-scheduler.yield
— ועדיין תומך בדפדפנים שלא מטמיעים אותו — תלוי במטרות שלך. אפשר להשתמש ב-Polyfill הרשמי. ה-Polyfill שימושי אם התנאים הבאים רלוונטיים למצב שלך:
- כבר נעשה שימוש ב-
scheduler.postTask
באפליקציה שלך כדי לתזמן משימות. - אתם רוצים להיות מסוגלים להגדיר עדיפויות למשימות ולתפוקה.
- ברצונך לבטל משימות או לקבוע להן סדרי עדיפויות דרך המחלקה
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.