תזמון JS טוב יותר עם isInputPending()

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

Nate Schloss
Nate Schloss
Andrew Comminos
Andrew Comminos

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

כדי למנוע את הצורך בתמורה הזו, Facebook הציעה והטמיעה את ה-API ‏isInputPending() ב-Chromium כדי לשפר את התגובה ללא פגיעה בביצועים. על סמך המשוב שקיבלנו מגרסת המקור לניסיון, ביצענו כמה עדכונים ב-API, ואנחנו שמחים להודיע שה-API מופיע עכשיו כברירת מחדל ב-Chromium 87.

תאימות דפדפן

תמיכה בדפדפנים

  • Chrome: 87.
  • Edge: ‏ 87.
  • Firefox: לא נתמך.
  • Safari: לא נתמך.

מקור

isInputPending() נשלחת בדפדפנים מבוססי Chromium החל מגרסה 87. אף דפדפן אחר לא סימן כוונה לשלוח את ה-API.

רקע

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

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

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

תרשים שבו מוצג שכשמריצים משימות JS ארוכות, לדפדפן יש פחות זמן לשלוח אירועים.

ב-Facebook רצינו לבדוק איך ייראו הדברים אם נמצא גישה חדשה לטעינת מודעות שתבטל את הפשרה המרגיזה הזו. פנו אלינו החברים שלנו ב-Chrome בנושא הזה, ויצאנו עם ההצעה ל-isInputPending(). ממשק ה-API של isInputPending() הוא הראשון שמשתמש במושג ההפרעות להזנת משתמשים באינטרנט, ומאפשר ל-JavaScript לבדוק אם יש קלט בלי להעביר את השליטה לדפדפן.

תרשים שבו מוצג שהפונקציה isInputPending() מאפשרת ל-JS לבדוק אם יש קלט משתמש בהמתנה, בלי להעביר את הביצועים בחזרה לדפדפן.

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

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

דוגמה: מתזמן עם תשואה גבוהה יותר

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

const DEADLINE = performance.now() + QUANTUM;
while (workQueue.length > 0) {
  if (performance.now() >= DEADLINE) {
    // Yield the event loop if we're out of time.
    setTimeout(processWorkQueue);
    return;
  }
  let job = workQueue.shift();
  job.execute();
}

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

זה בסדר, אבל אפשר לעשות טוב יותר? בהחלט!

const DEADLINE = performance.now() + QUANTUM;
while (workQueue.length > 0) {
  if (navigator.scheduling.isInputPending() || performance.now() >= DEADLINE) {
    // Yield if we have to handle an input event, or we're out of time.
    setTimeout(processWorkQueue);
    return;
  }
  let job = workQueue.shift();
  job.execute();
}

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

כברירת מחדל, אירועים 'מתמשכים' לא מוחזרים מ-isInputPending(). אלה כוללים את mousemove,‏ pointermove ועוד. אם תרצה להעביר את הבעלות עליהם גם כן, אין בעיה. מספקים אובייקט ל-isInputPending() עם הערך includeContinuous שמוגדר כ-true, וזהו:

const DEADLINE = performance.now() + QUANTUM;
const options = { includeContinuous: true };
while (workQueue.length > 0) {
  if (navigator.scheduling.isInputPending(options) || performance.now() >= DEADLINE) {
    // Yield if we have to handle an input event (any of them!), or we're out of time.
    setTimeout(processWorkQueue);
    return;
  }
  let job = workQueue.shift();
  job.execute();
}

זהו! מסגרות כמו React מפתחות תמיכה ב-isInputPending() בספריות הליבה שלהן לתזמון באמצעות לוגיקה דומה. אנחנו מקווים שהדבר יאפשר למפתחים שמשתמשים בפלטפורמות האלה ליהנות מ-isInputPending() מאחורי הקלעים בלי צורך בכתיבת מחדש משמעותית.

כניעה לא תמיד היא דבר רע

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

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

חשוב גם לשים לב לדפים אחרים שחולקים לולאת אירועים. בפלטפורמות כמו Chrome ל-Android, מקורות מרובים חולקים לעתים קרובות לולאת אירועים. isInputPending() אף פעם לא יחזיר את הערך true אם הקלט נשלח למסגרת ממקורות שונים, ולכן דפים ברקע עשויים להפריע לתגובה של דפים בחזית. כשאתם מבצעים פעולות ברקע באמצעות Page Visibility API, כדאי להפחית, לדחות או להעניק עדיפות לפעולות האלה בתדירות גבוהה יותר.

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

משוב

  • אפשר לשלוח משוב על המפרט במאגר is-input-pending.
  • אפשר לפנות אל ‎@acomminos (אחד ממחברי המפרט) ב-Twitter.

סיכום

אנחנו שמחים להשיק את isInputPending() ומאפשרים למפתחים להתחיל להשתמש בו כבר היום. זו הפעם הראשונה ש-Facebook יצרה ממשק API חדש לאינטרנט, והובילה אותו משלב הרעיון ועד להצעה לתקן, ואז עד להשקה בפועל בדפדפן. אנחנו רוצים להודות לכל מי שעזר לנו להגיע לשלב הזה, ולשלוח קריאת תיגר מיוחדת לכל הצוות של Chrome שעזר לנו לפתח את הרעיון הזה ולהוציא אותו לפועל.

התמונה הראשית (Hero) היא של Will H McMahan ב-Unsplash.