בניית אתרים שמגיבים במהירות לקלט של משתמשים היא אחד ההיבטים המאתגרים ביותר בביצועים באינטרנט – זה שצוות Chrome השקיע מאמצים רבים כדי לעזור למפתחי אינטרנט לעמוד בו. רק השנה הודענו שהמדד מהירות התגובה לאינטראקציה באתר (INP) ישתנה מסטטוס ניסיוני לסטטוס 'בהמתנה'. במרץ 2024, אנחנו מתכננים להחליף את מהירות התגובה לאינטראקציה ראשונה (FID) כמדד ליבה לבדיקת חוויית המשתמש באתר.
כחלק מהמאמץ המתמשך לספק ממשקי API חדשים שעוזרים למפתחי אתרים להפוך את האתרים שלהם למהירים ככל האפשר, צוות Chrome מפעיל כרגע גרסת מקור לניסיון של scheduler.yield
החל מגרסה 115 של Chrome. scheduler.yield
הוא הצעה חדשה ל-API של המתזמן, שמאפשרת להחזיר את השליטה ל-thread הראשי בדרך קלה וטובה יותר מאשר השיטות שעליהן התבססו בדרך כלל.
בתפוקה
JavaScript משתמש במודל הרצה עד להשלמה כדי לטפל במשימות. כלומר, כשמשימה מופעלת בשרשור הראשי, היא תימשך זמן רב יותר עד להשלמתה. כשמשימה מסתיימת, הבקרה חוזרת בחזרה ל-thread הראשי, וכך אתם יכולים לעבד את המשימה הבאה בתור המשימה הבאה בתור.
פרט למקרים קיצוניים שבהם משימה אף פעם לא מסתיימת – כמו לולאה אינסופית, לדוגמה – תפוקה היא היבט בלתי נמנע בלוגיקת תזמון המשימות של JavaScript. זה זה יקרה, רק מתי, ועדיף מוקדם יותר. ליתר ביטחון, משימות שנמשכות יותר מדי זמן – יותר מ-50 אלפיות השנייה – נחשבות למשימות ארוכות.
משימות ארוכות הן מקור תגובה איטית של דפים, כי הן מעכבות את יכולת הדפדפן להגיב לקלט של משתמשים. ככל שמשימות ארוכות יותר מתבצעות לעיתים קרובות יותר, וככל שהן נמשכות יותר זמן, כך עולה הסבירות שמשתמשים יקבלו את הרושם שהדף איטי או אפילו ירגישו שהוא לא תקין לגמרי.
עם זאת, רק בגלל שהקוד שלכם מפעיל משימה בדפדפן, זה לא אומר שצריך לחכות עד שהמשימה תסתיים לפני שהשליטה תועבר בחזרה לשרשור הראשי. אתם יכולים לשפר את הרספונסיביות לקלט של משתמשים בדף על ידי יצירת משימה מפורשת, שמאפשרת לסיים את המשימה בהזדמנות הבאה. כך משימות אחרות יכולות לקבל זמן בשרשור הראשי מוקדם יותר מאשר במצב שבו הן נאלצו להמתין עד שמשימות ארוכות יסתיימו.
![תיאור שבו מוסבר איך פיצול של משימה יכול לשפר את התגובה לקלט. בחלק העליון, משימה ארוכה מונעת מהגורם המטפל באירועים לפעול עד שהמשימה מסתיימת. בחלק התחתון, המשימה בחלוקה למקטעים מאפשרת ל-handler של האירוע לפעול מוקדם יותר ממה שהוא היה יכול לפעול.](https://developer.chrome.com/static/blog/introducing-scheduler-yield-origin-trial/image/a-depiction-how-breaking-1f8a83887e582.png?authuser=5&hl=he)
כשאתם מעבירים נכונות במפורש, אתם אומרים לדפדפן: "היי, אני מבין שהעבודה שאני עומד לבצע עשויה להימשך זמן מה, ואני לא רוצה שתצטרכו לבצע את כל העבודה הזאת לפני שאתם מגיבים לקלט של משתמשים או למשימות אחרות שעשויות להיות חשובות גם כן. זהו כלי חשוב בארגז הכלים של המפתחים שיכול לתרום מאוד לשיפור חוויית המשתמש.
הבעיה באסטרטגיות התפוקה הנוכחיות
שיטה נפוצה של תפוקה משתמשת ב-setTimeout
עם ערך הזמן הקצוב לתפוגה של 0
. הסיבה לכך היא שהקריאה החוזרת שהועברה אל setTimeout
תעביר את שאר העבודה למשימה נפרדת שתמתין להפעלה הבאה. במקום להמתין שהדפדפן יעבוד מעצמו, אתם אומרים "שנפצל את חלק העבודה הגדול הזה לחלקים קטנים יותר".
עם זאת, לתפוקה עם setTimeout
יכולה להיות תופעת לוואי לא רצויה: העבודה שתופיע אחרי נקודת ההכנה תועבר לחלק האחורי של תור המשימות. משימות שתוזמנו על ידי אינטראקציות של משתמשים עדיין יועברו לחלק העליון של התור, אבל את שאר העבודה שרציתם לבצע אחרי שהגיעו אליה במפורש עלולה להיות עיכובים נוספים כתוצאה ממשימות אחרות ממקורות מתחרים שממתינים בתור.
כדי לראות את זה בפעולה, אפשר לנסות את ההדגמה הזו של תקלה או להתנסות בה בגרסה המוטמעת שלמטה. ההדגמה מורכבת מכמה לחצנים שאתם יכולים ללחוץ עליהם, ומתחתיהם תיבה שמתעדת מתי משימות מבוצעות. כשמגיעים לדף, מבצעים את הפעולות הבאות:
- לוחצים על הלחצן העליון שנקרא הפעלת משימות מדי פעם. הלחצן הזה יתזמן את המשימות כך שיפעלו כל כמה זמן. כשלוחצים על הלחצן הזה, יומן המשימות יאוכלס בכמה הודעות עם הכיתוב הרצת משימות החסימה באמצעות
setInterval
. - לאחר מכן, לוחצים על הלחצן 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
בפעולה, יש לבצע את הפעולות הבאות:
- נווט אל
chrome://flags
. - הפעלת הניסוי תכונות ניסיוניות של פלטפורמת האינטרנט. ייתכן שתצטרכו להפעיל מחדש את Chrome לאחר מכן.
- אפשר לעבור אל דף ההדגמה או להשתמש בגרסה המוטמעת שלו שמופיעה מתחת לרשימה הזו.
- לוחצים על הלחצן העליון עם הכיתוב הפעלת משימות מדי פעם.
- בסיום, לוחצים על הלחצן 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:
- כדי להתנסות ב-
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
הוא תוספת מלהיבה ל-API של המתזמן – ממשק כזה שיקל על המפתחים לשפר את הרספונסיביות בהשוואה לאסטרטגיות התפוקה הנוכחיות. אם scheduler.yield
נראה לך כ-API מועיל, אנחנו מזמינים אותך להשתתף במחקר שלנו כדי לשפר אותו, ולשלוח משוב לגבי דרכים לשיפור.
תמונה ראשית (Hero) מ-Unbounce, מאת Jonathan Allison.