באפליקציות ובאתרים רבים יש הרבה סקריפטים להפעלה. לעיתים קרובות יש להפעיל את JavaScript בהקדם האפשרי, אבל בו-זמנית אינכם רוצים שהוא יפריע למשתמש. אם שולחים נתוני ניתוח כאשר המשתמש גולל את הדף, או מוסיפים רכיבים ל-DOM בזמן שהם מקישים על הלחצן, אפליקציית האינטרנט עשויה להפסיק להגיב, וכתוצאה מכך חוויית המשתמש נפגעת.
החדשות הטובות הן שעכשיו יש API שיכול לעזור: requestIdleCallback
. באותו אופן שבו השימוש ב-requestAnimationFrame
אפשר לנו לתזמן אנימציות בצורה נכונה ולמקסם את הסיכויים שלנו להגיע לקצב של 60fps, גם requestIdleCallback
תתזמן עבודה כשיש זמן פנוי בסוף פריים, או כשהמשתמש לא פעיל. המשמעות היא שיש הזדמנות לבצע את העבודה בלי להפריע למשתמש. הוא זמין החל מגרסה 47 של Chrome, כך שמומלץ להתנסות בו כבר היום באמצעות Chrome Canary! מדובר בתכונה ניסיונית והמפרט עדיין בתהליכי שינויים, כך שדברים עשויים להשתנות בעתיד.
למה כדאי להשתמש ב-requestIdleCallback?
קשה מאוד לקבוע עבודה לא חיונית בעצמכם. אי אפשר לדעת בדיוק כמה זמן רינדור פריים נותר, כי אחרי הפעלת הקריאות החוזרות של requestAnimationFrame
יש חישובי סגנון, פריסה, צבע ורכיבים פנימיים נוספים של הדפדפן שצריך להפעיל. פתרון שמסובב על מסך הבית לא יכול להביא בחשבון אף אחת מהאפשרויות האלה. כדי לוודא שלמשתמש לא תהיה אינטראקציה כלשהי, תצטרכו לצרף מאזינים לכל סוג של אירוע אינטראקציה (scroll
, touch
, click
), גם אם אתם לא צריכים אותם למטרות פונקציונליות, רק כך שתוכלו להיות בטוחים לחלוטין שהמשתמש לא מבצע אינטראקציה. הדפדפן, לעומת זאת, יודע בדיוק כמה זמן זמין בסוף הפריים, ואם למשתמש יש אינטראקציה. לכן באמצעות requestIdleCallback
אנחנו מקבלים API שמאפשר לנו לנצל את הזמן הפנוי בצורה היעילה ביותר.
נבחן את הנושא לעומק ונראה איך נוכל להשתמש בו.
המערכת בודקת אם יש requestIdleCallback
אלה ימים מוקדמים של requestIdleCallback
, לכן לפני שמשתמשים בו כדאי לבדוק שהוא זמין לשימוש:
if ('requestIdleCallback' in window) {
// Use requestIdleCallback to schedule work.
} else {
// Do what you’d do today.
}
אפשר גם לשנות את אופן הפעולה של האפליקציה, כדי לעשות זאת באמצעות חזרה אל setTimeout
:
window.requestIdleCallback =
window.requestIdleCallback ||
function (cb) {
var start = Date.now();
return setTimeout(function () {
cb({
didTimeout: false,
timeRemaining: function () {
return Math.max(0, 50 - (Date.now() - start));
}
});
}, 1);
}
window.cancelIdleCallback =
window.cancelIdleCallback ||
function (id) {
clearTimeout(id);
}
לא מומלץ להשתמש ב-setTimeout
, כי לא ידוע על זמן ללא פעילות כמו requestIdleCallback
. עם זאת, צריך להתקשר ישירות לפונקציה אם requestIdleCallback
לא זמין, כך שתהיה לך אפשרות להמשיך בצורה בטוחה. בזמן השימוש ב-shim, האפשרות requestIdleCallback
תהיה זמינה, והשיחות יופנו באופן שקט, וזה מצב מצוין.
אבל בינתיים, נניח שהיא קיימת.
שימוש ב-requestIdleCallback
הקריאה ל-requestIdleCallback
דומה מאוד ל-requestAnimationFrame
בכך שהיא לוקחת פונקציית קריאה חוזרת כפרמטר הראשון שלה:
requestIdleCallback(myNonEssentialWork);
כשקוראים לפונקציה myNonEssentialWork
, הוא מקבל אובייקט deadline
שמכיל פונקציה שמחזירה מספר שמציין כמה זמן נותר לעבודה:
function myNonEssentialWork (deadline) {
while (deadline.timeRemaining() > 0)
doWorkIfNeeded();
}
אפשר לשלוח קריאה לפונקציה timeRemaining
כדי לקבל את הערך העדכני ביותר. אם הפונקציה timeRemaining()
מחזירה אפס, אפשר לתזמן עוד requestIdleCallback
אם יש עוד מה לעשות:
function myNonEssentialWork (deadline) {
while (deadline.timeRemaining() > 0 && tasks.length > 0)
doWorkIfNeeded();
if (tasks.length > 0)
requestIdleCallback(myNonEssentialWork);
}
הבטחת הפונקציה שלך נקראת
מה עושים כשיש ממש עומס? יכול להיות שאתם חוששים שלא יתקשרו אליכם בחזרה. ובכן, requestIdleCallback
דומה ל-requestAnimationFrame
, אבל הוא שונה גם בכך שהוא מקבל פרמטר שני אופציונלי: אובייקט אפשרויות עם מאפיין זמן קצוב לתפוגה. אם הוא מוגדר, הזמן הקצוב לתפוגה הזה נותן לדפדפן זמן באלפיות השנייה שבו הוא צריך לבצע את הקריאה החוזרת:
// Wait at most two seconds before processing events.
requestIdleCallback(processPendingAnalyticsEvents, { timeout: 2000 });
אם הקריאה החוזרת (callback) מתבצעת בגלל ההפעלה של הזמן הקצוב לתפוגה, תראו שני דברים:
- הפונקציה
timeRemaining()
תחזיר את הערך אפס. - המאפיין
didTimeout
של האובייקטdeadline
יתקיים.
אם רואים שה-didTimeout
נכון, סביר להניח שעדיף פשוט להריץ את העבודה ולעשות איתה:
function myNonEssentialWork (deadline) {
// Use any remaining time, or, if timed out, just run through the tasks.
while ((deadline.timeRemaining() > 0 || deadline.didTimeout) &&
tasks.length > 0)
doWorkIfNeeded();
if (tasks.length > 0)
requestIdleCallback(myNonEssentialWork);
}
בגלל ההפרעה הפוטנציאלית הזו, הזמן הקצוב לתפוגה עשוי לגרום למשתמשים (העבודה עלולה לגרום לאפליקציה להפסיק להגיב או לגרום לתקלה). חשוב להיזהר כשמגדירים את הפרמטר הזה. כשהדבר אפשרי, צריך לאפשר לדפדפן להחליט מתי לבצע את הקריאה החוזרת.
שימוש ב-requestIdleCallback לשליחת ניתוח נתונים
נבחן את אפשרות השימוש ב-requestIdleCallback
לשליחת ניתוח נתונים. במקרה כזה, סביר להניח שנרצה לעקוב אחרי אירוע כמו – למשל – הקשה על תפריט ניווט. עם זאת, מכיוון שבדרך כלל מוסיפים אנימציה למסך הזה, מומלץ להימנע משליחת האירוע הזה ל-Google Analytics באופן מיידי. אנחנו ניצור מערך של אירועים כדי לשלוח ולבקש שהם יישלחו בשלב כלשהו בעתיד:
var eventsToSend = [];
function onNavOpenClick () {
// Animate the menu.
menu.classList.add('open');
// Store the event for later.
eventsToSend.push(
{
category: 'button',
action: 'click',
label: 'nav',
value: 'open'
});
schedulePendingEvents();
}
עכשיו נצטרך להשתמש בכתובת requestIdleCallback
כדי לעבד אירועים בהמתנה:
function schedulePendingEvents() {
// Only schedule the rIC if one has not already been set.
if (isRequestIdleCallbackScheduled)
return;
isRequestIdleCallbackScheduled = true;
if ('requestIdleCallback' in window) {
// Wait at most two seconds before processing events.
requestIdleCallback(processPendingAnalyticsEvents, { timeout: 2000 });
} else {
processPendingAnalyticsEvents();
}
}
כאן ניתן לראות שהגדרתי זמן קצוב לתפוגה של 2 שניות, אבל הערך הזה תלוי באפליקציה שלך. כשמדובר בנתוני ניתוח, כדאי להגדיר זמן קצוב לתפוגה כדי להבטיח שהנתונים ידווחו בפרק זמן סביר ולא רק בשלב כלשהו בעתיד.
לבסוף, אנחנו צריכים לכתוב את הפונקציה ש-requestIdleCallback
יפעיל.
function processPendingAnalyticsEvents (deadline) {
// Reset the boolean so future rICs can be set.
isRequestIdleCallbackScheduled = false;
// If there is no deadline, just run as long as necessary.
// This will be the case if requestIdleCallback doesn’t exist.
if (typeof deadline === 'undefined')
deadline = { timeRemaining: function () { return Number.MAX_VALUE } };
// Go for as long as there is time remaining and work to do.
while (deadline.timeRemaining() > 0 && eventsToSend.length > 0) {
var evt = eventsToSend.pop();
ga('send', 'event',
evt.category,
evt.action,
evt.label,
evt.value);
}
// Check if there are more events still to send.
if (eventsToSend.length > 0)
schedulePendingEvents();
}
בדוגמה הזו, הנחתי שאם לא קיים requestIdleCallback
, צריך לשלוח את ניתוח הנתונים באופן מיידי. עם זאת, באפליקציות ייצור עדיף לעכב את השליחה עם זמן קצוב לתפוגה כדי להבטיח שהיא לא מתנגשת עם אינטראקציות וגורמת לבעיות.
שימוש ב-requestIdleCallback לביצוע שינויי DOM
מצב נוסף שבו requestIdleCallback
יכול לשפר את הביצועים באופן משמעותי הוא כשמבצעים שינויי DOM לא חיוניים, כמו הוספת פריטים לסופה של רשימה שנרחבת כל הזמן והטעינה נמשכת. בואו נראה איך requestIdleCallback
משתלבת בפועל במסגרת טיפוסית.
ייתכן שהדפדפן יהיה עמוס מדי ולא יוכל להפעיל קריאות חוזרות (callback) במסגרת מסוימת, לכן לא בטוח שיהיה כל זמן פנוי בסוף הפריים לביצוע עבודה נוספת. לכן הוא שונה ממחרוזת כמו setImmediate
, שפועלת בכל פריים.
אם הקריאה החוזרת מופעלת בסוף הפריים, היא תתוזמן לביצוע לאחר יצירת הפריים הנוכחי, המשמעות היא ששינויי הסגנון יחולו, וחשוב גם שהפריסה תחושב. אם נבצע שינויי DOM בתוך הקריאה החוזרת (callback) ללא פעילות, חישובי הפריסה האלה יבוטלו. אם יש קריאות פריסה מסוג כלשהו בפריים הבא, למשל. getBoundingClientRect
, clientWidth
וכו', הדפדפן יצטרך לבצע פריסה סינכרונית מאולצת, שהיא צוואר בקבוק פוטנציאלי בביצועים.
סיבה נוספת לכך ששינויי DOM לא פעילים בקריאה חוזרת (callback) ללא פעילות היא שהשפעת הזמן של שינוי ה-DOM היא בלתי צפויה, ולכן אנחנו יכולים בקלות לעבור את המועד האחרון שסופק על ידי הדפדפן.
השיטה המומלצת היא לבצע שינויי DOM רק בתוך קריאה חוזרת (callback) של requestAnimationFrame
, כי הדפדפן מתזמן את הפעולה הזו תוך התייחסות לסוג העבודה הזה. כלומר, הקוד שלנו צריך להשתמש בקטע של מסמך, שאותו ניתן לצרף בקריאה החוזרת הבאה של requestAnimationFrame
. אם משתמשים בספריית VDOM, צריך להשתמש ב-requestIdleCallback
כדי לבצע שינויים, אבל צריך להחיל את תיקוני ה-DOM בקריאה החוזרת הבאה של requestAnimationFrame
, ולא בקריאה החוזרת (callback) ללא פעילות.
אחרי הדברים האלה, נסתכל על הקוד:
function processPendingElements (deadline) {
// If there is no deadline, just run as long as necessary.
if (typeof deadline === 'undefined')
deadline = { timeRemaining: function () { return Number.MAX_VALUE } };
if (!documentFragment)
documentFragment = document.createDocumentFragment();
// Go for as long as there is time remaining and work to do.
while (deadline.timeRemaining() > 0 && elementsToAdd.length > 0) {
// Create the element.
var elToAdd = elementsToAdd.pop();
var el = document.createElement(elToAdd.tag);
el.textContent = elToAdd.content;
// Add it to the fragment.
documentFragment.appendChild(el);
// Don't append to the document immediately, wait for the next
// requestAnimationFrame callback.
scheduleVisualUpdateIfNeeded();
}
// Check if there are more events still to send.
if (elementsToAdd.length > 0)
scheduleElementCreation();
}
כאן אני יוצר את הרכיב ומשתמש במאפיין textContent
כדי לאכלס אותו, אבל רוב הסיכויים שקוד יצירת הרכיב שלכם יהיה מעורב יותר! אחרי יצירת הרכיב scheduleVisualUpdateIfNeeded
, מתבצעת קריאה חוזרת (callback) אחת של requestAnimationFrame
, ובעקבות זאת היא תצרף את שבר המסמך לגוף:
function scheduleVisualUpdateIfNeeded() {
if (isVisualUpdateScheduled)
return;
isVisualUpdateScheduled = true;
requestAnimationFrame(appendDocumentFragment);
}
function appendDocumentFragment() {
// Append the fragment and reset.
document.body.appendChild(documentFragment);
documentFragment = null;
}
אם הכול תקין, נראה עכשיו הרבה פחות בעיות כשמצרפים פריטים ל-DOM. מצוין!
שאלות נפוצות
- האם יש polyfill?
לצערנו, יש ספריית shim אם אתם רוצים להגדיר הפניה אוטומטית שקופה אל
setTimeout
. ה-API הזה קיים כי הוא חוסם פער אמיתי מאוד בפלטפורמת האינטרנט. קשה להסיק חוסר פעילות, אבל לא קיימים ממשקי API של JavaScript כדי לקבוע את משך הזמן הפנוי בסוף הפריים, כך שבמקרה הצורך כדאי לנחש. אפשר להשתמש בממשקי API כמוsetTimeout
,setInterval
אוsetImmediate
כדי לתזמן עבודות, אבל הם לא מוגבלים בזמן כדי למנוע אינטראקציה של המשתמשים בצורה שמוגדרת ב-requestIdleCallback
. - מה יקרה אם אעבור את תאריך היעד?
אם הפונקציה
timeRemaining()
מחזירה אפס אבל בחרתם לפעול למשך זמן ארוך יותר, אפשר לעשות זאת מבלי לחשוש שהדפדפן יעצור את העבודה. עם זאת, הדפדפן מספק את תאריך היעד שבו ניתן לנסות ולהבטיח למשתמשים חוויה חלקה. לכן, אלא אם יש סיבה טובה מאוד לכך, תמיד חשוב לפעול בהתאם לתאריך היעד. - האם יש ערך מקסימלי שתחזיר
timeRemaining()
? כן, עכשיו הזמן הוא 50 אלפיות השנייה. כשמנסים לנהל אפליקציה רספונסיבית, כדאי שכל התגובות לאינטראקציות של המשתמשים יהיו באורך של פחות מ-100 אלפיות השנייה. אם יש למשתמש אינטראקציה בחלון של 50 אלפיות השנייה, ברוב המקרים יש לאפשר את השלמת הקריאה החוזרת (callback) ללא פעילות והדפדפן יגיב לאינטראקציות של המשתמש. יכול להיות שתוזמנו כמה קריאות חוזרות (callback) ללא פעילות (אם הדפדפן יקבע שיש מספיק זמן להפעיל אותן). - יש סוג של עבודה שלא כדאי לבצע ב-requestIdleCallback?
באופן אידיאלי, העבודה צריכה להיות במקטעים קטנים (מיקרו-משימות) עם מאפיינים צפויים יחסית. לדוגמה, כאשר משנים את ה-DOM באופן ספציפי יהיו זמני ביצוע לא צפויים, מאחר שהוא יגרום לחישובי סגנון, פריסה, ציור והרכבת. לכן, צריך לבצע שינויי DOM רק בקריאה חוזרת (callback) של
requestAnimationFrame
כפי שהוצע למעלה. דבר נוסף שצריך להיזהר ממנו הוא פתרון (או דחייה) של הבטחות, כי הקריאות החוזרות (callback) יתבצעו מיד לאחר שהקריאה החוזרת (callback) ללא פעילות תסתיים, גם אם לא נותר זמן נוסף. - האם תמיד מקבלים
requestIdleCallback
בסוף פריים? לא, לא תמיד. הדפדפן יתזמן את הקריאה החוזרת בכל פעם שיש זמן פנוי בסוף פריים, או בתקופות שבהן המשתמש לא פעיל. לא כדאי לצפות שהקריאה החוזרת תתבצע לכל פריים. אם אתם דורשים שהקריאה תפעל במסגרת זמן מסוימת, צריך לנצל את הזמן הקצוב לתפוגה. - אפשר לקבל כמה קריאות חוזרות של
requestIdleCallback
? כן, אפשר, עד כמה שניתן, ניתן להגדיר מספר קריאות חוזרות שלrequestAnimationFrame
. עם זאת, כדאי לזכור שאם הקריאה החוזרת הראשונה גורמת לנצל את הזמן שנותר לביצוע הקריאה החוזרת, לא יהיה יותר זמן לשיחות חוזרות אחרות. לאחר מכן, הקריאות החוזרות האחרות יצטרכו להמתין עד שהדפדפן יפסיק להיות פעיל הבא לפני שניתן יהיה להפעיל אותן. בהתאם לעבודה שניסית לבצע, יכול להיות שעדיף להשתמש לקריאה חוזרת (callback) אחת ללא פעילות ולחלק את העבודה ביניכם. לחלופין, אפשר להשתמש בזמן הקצוב לתפוגה כדי להבטיח שאף קריאה חוזרת לא תקבל רעב לזמן. - מה קורה אם מגדירים קריאה חוזרת (callback) חדשה ללא פעילות בתוך שיחה אחרת? הקריאה החוזרת (callback) החדשה ללא פעילות תוזמנה לפעול בהקדם האפשרי, החל מהפריים הבא (במקום מהפריים הנוכחי).
לא פעיל!
requestIdleCallback
היא דרך מעולה להריץ את הקוד אבל בלי להפריע למשתמש. הוא פשוט לשימוש ומאוד גמיש. עם זאת, אלה עדיין ימים מוקדמים, והמפרט עדיין לא הוסדר בצורה מלאה, כך שכל משוב שלך יתקבל בברכה.
אתם מוזמנים לראות אותו ב-Chrome Canary, להתנסות בפרויקטים שלכם, לספר לנו איך אתם מתקדמים.