שימוש ב-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 בסוף פריים? לא, לא תמיד. הדפדפן יתזמן את הקריאה החוזרת בכל פעם שיש זמן פנוי בסוף פריים, או בתקופות שבהן המשתמש לא פעיל. אין לצפות שהקריאה הלא מידית תתבצע לכל פריים, ואם אתם צריכים שהיא תרוץ במסגרת זמן מסוימת, כדאי להשתמש בזמן הקצוב לתפוגה.
  • האם אפשר להגדיר מספר קריאות חוזרות של requestIdleCallback? כן, אפשר, בדיוק כמו שאפשר להגדיר כמה קריאות חוזרות של requestAnimationFrame. עם זאת, חשוב לזכור שאם השיחה החוזרת הראשונה תשתמש בכל הזמן שנותר במהלך השיחה החוזרת, לא יישאר זמן לשיחות חוזרות אחרות. לאחר מכן, שאר הפונקציות החזרה (callbacks) יצטרכו להמתין עד שהדפדפן יהיה במצב חוסר פעילות בפעם הבאה כדי שיהיה אפשר להריץ אותן. בהתאם לעבודה שאתם מנסים לבצע, יכול להיות שיהיה עדיף להשתמש בקריאה חוזרת אחת במצב חוסר פעילות ולחלק את העבודה שם. לחלופין, אפשר להשתמש בזמן הקצוב לתפוגה כדי לוודא שלא יהיו קריאות חזרה (callbacks) שיאבדו זמן.
  • מה קורה אם מגדירים קריאה חוזרת חדשה בזמן חוסר פעילות בתוך קריאה חוזרת אחרת? הקריאה החוזרת החדשה לפעולה במצב חוסר פעילות תתוזמן לפעול בהקדם האפשרי, החל מהפריים הבא (ולא מהפריים הנוכחי).

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

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

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