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

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

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

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

מסגרת רגילה.

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

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

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

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

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

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