שימוש ב-requestIdleCallback

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

שימוש ב-requestIdleCallback כדי לתזמן עבודה לא חיונית.

החדשות הטובות הן שיש עכשיו ממשק 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 בדרך הזו לא גרוע יותר. בעזרת ה-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, אבל יש גם הבדל: הוא מקבל פרמטר שני אופציונלי: אובייקט אפשרויות עם מאפיין זמן קצוב. אם מגדירים את הזמן הקצוב לתפוגה, הדפדפן מקבל זמן באלפיות שנייה שבו הוא צריך לבצע את קריאת החזרה (callback):

// Wait at most two seconds before processing events.
requestIdleCallback(processPendingAnalyticsEvents, { timeout: 2000 });

אם פונקציית ה-callback תתבצע בגלל תפוגת הזמן, תבחינו בשני דברים:

  • הפונקציה timeRemaining() תחזיר אפס.
  • המאפיין didTimeout של האובייקט deadline יהיה true.

אם הערך של didTimeout הוא True, סביר להניח שתרצו פשוט להריץ את העבודה ולהסיר אותה:

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 לא קיים, נתוני Analytics צריכים להישלח באופן מיידי. עם זאת, באפליקציה בסביבת הייצור, מומלץ לדחות את השליחה עם זמן קצוב לתפוגה כדי לוודא שהיא לא תיצור התנגשויות עם אינטראקציות כלשהן ותגרום לתנודות.

שימוש ב-requestIdleCallback כדי לבצע שינויים ב-DOM

מצב נוסף שבו requestIdleCallback יכול לשפר את הביצועים הוא כשצריך לבצע שינויים לא חיוניים ב-DOM, כמו הוספת פריטים לסוף רשימה הולכת וגדלה שנטענת באיטרציה. נראה איך requestIdleCallback נכנס למסגרת רגילה.

מסגרת רגילה.

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

אם הקריאה החוזרת מופעלת בסוף המסגרת, היא תתוזמן לפעול אחרי שהמסגרת הנוכחית תאושר, כלומר אחרי שהשינויים בסגנון יחולו, וחשוב מכך, אחרי שהפריסה תחושב. אם יבוצעו שינויים ב-DOM בתוך פונקציית ה-callback לזמן לא פעיל, חישובי הפריסה האלה יבוטלו. אם יש קריאות של פריסות מסוג כלשהו בפריים הבא, למשל getBoundingClientRect,‏ clientWidth וכו', הדפדפן יצטרך לבצע פריסה סינכרונית מאולצת, שעלולה להוביל לצוואר בקבוק בביצועים.

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

מומלץ לבצע שינויים ב-DOM רק בתוך קריאה חוזרת (callback) של requestAnimationFrame, כי הדפדפן מתזמן אותה מתוך מחשבה על סוג העבודה הזה. כלומר, הקוד שלנו יצטרך להשתמש בחלק מסמך, שאפשר יהיה לצרף אותו בקריאה החוזרת הבאה של requestAnimationFrame. אם אתם משתמשים בספריית VDOM, אתם משתמשים ב-requestIdleCallback כדי לבצע שינויים, אבל מחילים את התיקונים של DOM בקריאה הבאה בחזרה (callback) של 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, שמגדירה קריאה חוזרת יחידה של 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? לצערי לא, אבל יש פתרון אם רוצים לבצע הפניה אוטומטית שקופה אל setTimeout. הסיבה לקיומו של ה-API הזה היא שהוא ממלא פער אמיתי בפלטפורמת האינטרנט. קשה להסיק על חוסר פעילות, אבל אין ממשקי API של JavaScript שיכולים לקבוע את משך הזמן הפנוי בסוף המסגרת, ולכן במקרה הטוב תצטרכו להסתמך על ניחושים. אפשר להשתמש בממשקי API כמו setTimeout,‏ setInterval או setImmediate כדי לתזמן משימות, אבל הם לא מתוזמנים כך שימנעו אינטראקציה של משתמשים כמו requestIdleCallback.
  • מה קורה אם לא עומדים במועד האחרון? אם הפונקציה timeRemaining() מחזירה אפס, אבל אתם בוחרים להריץ את הקוד למשך זמן ארוך יותר, תוכלו לעשות זאת בלי חשש שהדפדפן יפסיק את העבודה שלכם. עם זאת, הדפדפן נותן לכם את מועד היעד כדי לנסות להבטיח חוויה חלקה למשתמשים, לכן, אלא אם יש סיבה טובה מאוד, תמיד כדאי לפעול בהתאם למועד היעד.
  • האם יש ערך מקסימלי ש-timeRemaining() תחזיר? כן, כרגע הוא 50ms. כדי לשמור על אפליקציה רספונסיבית, כל התגובות לאינטראקציות של משתמשים צריכות להישאר מתחת ל-100 אלפיות השנייה. אם המשתמש יבצע אינטראקציה, ברוב המקרים חלון ה-50 אלפיות השנייה אמור לאפשר להשלמת הקריאה החוזרת ללא פעילות, ולדפדפן להגיב לאינטראקציות של המשתמש. יכול להיות שתקבלו מספר קריאות חוזרות ללא פעילות (idle) שתוזמנו ברצף (אם הדפדפן יקבע שיש מספיק זמן להריץ אותן).
  • האם יש סוגים מסוימים של עבודה שאסור לבצע ב-requestIdleCallback? באופן אידיאלי, כדאי לפצל את העבודה למשימות קטנות (מיקרו-משימות) עם מאפיינים ניתנים לחיזוי. לדוגמה, שינוי DOM בפרט יגרום לזמני ביצוע בלתי צפויים, כי הוא יגרום לחישוב סגנונות, פריסה, ציור ויצירת קומפוזיציה. לכן, צריך לבצע שינויים ב-DOM רק בקריאה חוזרת (callback) של requestAnimationFrame, כפי שהצענו למעלה. חשוב גם להיזהר מפתרון (או דחייה) של Promises, כי הפונקציות החזרה (callbacks) יבוצעו מיד אחרי שהפונקציה החזרה (callback) של מצב הפעילות השקטה תסתיים, גם אם לא נותר זמן.
  • האם תמיד יופיע requestIdleCallback בסוף פריים? לא, לא תמיד. הדפדפן יתזמן את הקריאה החוזרת בכל פעם שיש זמן פנוי בסוף פריים, או בתקופות שבהן המשתמש לא פעיל. אין לצפות שהקריאה לאחזור נתונים (callback) תתבצע לכל פריים, ואם אתם צריכים שהיא תפעל במסגרת זמן מסוימת, כדאי להשתמש בזמן קצוב לתפוגה.
  • האם אפשר להגדיר מספר קריאות חוזרות של requestIdleCallback? כן, אפשר, בדיוק כמו שאפשר להגדיר כמה קריאות חוזרות של requestAnimationFrame. עם זאת, חשוב לזכור שאם השיחה החוזרת הראשונה תשתמש בכל הזמן שנותר במהלך השיחה החוזרת, לא יישאר זמן לשיחות חוזרות נוספות. לאחר מכן, שאר הפונקציות החזרה (callbacks) יצטרכו להמתין עד שהדפדפן יהיה במצב חוסר פעילות בפעם הבאה כדי שיהיה אפשר להריץ אותן. בהתאם לעבודה שאתם מנסים לבצע, יכול להיות שיהיה עדיף להשתמש בקריאה חוזרת אחת במצב חוסר פעילות ולחלק את העבודה שם. לחלופין, אפשר להשתמש בזמן הקצוב לתפוגה כדי לוודא שלא יהיו קריאות חזרה (callbacks) שיאבדו זמן.
  • מה קורה אם מגדירים קריאה חוזרת חדשה בזמן חוסר פעילות בתוך קריאה חוזרת אחרת? הקריאה החוזרת החדשה לפעולה במצב חוסר פעילות תתוזמן לפעול בהקדם האפשרי, החל מהפריים הבא (ולא מהפריים הנוכחי).

קדימה, קדימה!

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

כדאי לבדוק את התכונה ב-Chrome Canary, לנסות אותה בפרויקטים שלכם ולספר לנו איך היא עובדת.