שימוש ב-requestIdleCallback

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

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

החדשות הטובות הן שעכשיו יש API שיכול לעזור: requestIdleCallback. בדיוק כמו שאימוץ השימוש ב-requestAnimationFrame אפשר לנו לתזמן אנימציות כראוי ולמקסם את הסיכויים שלנו להגיע ל-60fps, גם requestIdleCallback יתזמן עבודה כאשר יש זמן פנוי בסוף הפריים, או כשהמשתמש לא פעיל. פירוש הדבר הוא שיש הזדמנות לבצע את העבודה שלכם מבלי להפריע למשתמש. הוא זמין החל מגרסה 47 של Chrome, כך שאפשר להתחיל לשחק בו עוד היום בעזרת Chrome Canary! זוהי תכונה ניסיונית, והמפרט עדיין עדכני, כך שדברים עשויים להשתנות בעתיד.

למה כדאי להשתמש ב-requestIdleCallback?

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

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

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

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

אם 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);
}

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

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

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

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

מסגרת טיפוסית.

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

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

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

השיטה המומלצת היא לבצע שינויי DOM רק בתוך קריאה חוזרת (callback) של requestAnimationFrame, מאחר שפעולה זו מתוזמנת על ידי הדפדפן תוך התחשבות בסוג העבודה הזה. כלומר, הקוד שלנו יצטרך להשתמש במקטע של מסמך, שאותו אפשר להוסיף בקריאה החוזרת (callback) הבאה של requestAnimationFrame. אם אתם משתמשים בספריית VDOM, תצטרכו להשתמש ב-requestIdleCallback כדי לבצע שינויים, אבל להחיל את תיקוני ה-DOM בקריאה החוזרת הבאה של requestAnimationFrame, ולא את הקריאה החוזרת ללא פעילות.

לכן, בואו נסתכל על הקוד:

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;
}

כשהכול תקין, עכשיו נראה הרבה פחות בעיות jank כשמצרפים פריטים ל-DOM. מעולה!

שאלות נפוצות

  • האם יש polyfill? למרבה הצער, לא, אבל יש shim אם אתם רוצים הפניה שקופה אל setTimeout. הסיבה לכך שה-API הזה קיים היא שהוא מחבר פער אמיתי מאוד בפלטפורמת האינטרנט. קשה להסיק מהו היעדר פעילות, אך אין ממשקי API של JavaScript שנועדו לקבוע את כמות הזמן הפנוי בסוף המסגרת, כך שבמקרה הטוב ביותר תצטרכו לנחש. אפשר להשתמש בממשקי API כמו setTimeout, setInterval או setImmediate כדי לתזמן עבודה, אבל הם לא מתוזמנים מראש כדי למנוע אינטראקציות של המשתמשים באופן דומה לזה של requestIdleCallback.
  • מה קורה אם חורגים מהמועד האחרון? אם timeRemaining() מחזירה אפס אבל בחרת לפעול למשך זמן רב יותר, אפשר לעשות זאת מבלי לחשוש שהדפדפן יפסיק את פעולתך. עם זאת, הדפדפן נותן לכם את תאריך היעד בניסיון להבטיח חוויה חלקה למשתמשים. לכן, אלא אם יש סיבה טובה מאוד, מומלץ תמיד לפעול בהתאם למועד האחרון.
  • האם יש ערך מקסימלי שיוחזר על ידי timeRemaining()? כן, משך הזמן הוא עכשיו 50 אלפיות השנייה. כשמנסים לתחזק אפליקציה רספונסיבית, כל התגובות לאינטראקציות של המשתמשים צריכות להיות באורך של פחות מ-100 אלפיות השנייה. אם המשתמש מקיים אינטראקציה עם חלון של 50 אלפיות השנייה, ברוב המקרים צריך לאפשר את השלמת הקריאה החוזרת (callback) ללא פעילות, והדפדפן יגיב לאינטראקציות של המשתמש. יכול להיות שיופיעו כמה קריאות חוזרות (callback) ללא פעילות, שתוזמנו חזרה אחורה (אם הדפדפן יקבע שיש מספיק זמן להפעיל אותן).
  • האם יש סוג של עבודה שלא צריך לעשות ב-requestIdleCallback? באופן אידיאלי, העבודה צריכה להיות במקטעים קטנים (מיקרו-משימות) שיש להם מאפיינים צפויים יחסית. לדוגמה, אם משנים את ה-DOM, זמני הביצוע יהיו לא צפויים, כי השינוי יפעיל חישובי סגנון, פריסה, ציור ואיחוד. לכן יש לבצע שינויים ב-DOM רק בקריאה חוזרת (callback) של requestAnimationFrame כפי שהוצע למעלה. דבר נוסף שצריך להיזהר ממנו הוא פתרון (או דחייה) של הבטחות, מכיוון שהקריאות החוזרות יבוצעו מיד לאחר סיום הקריאה החוזרת הלא פעילה, גם אם לא נותר עוד זמן.
  • האם תמיד אקבל requestIdleCallback בסוף הפריים? לא, לא תמיד. הדפדפן יתזמן את הקריאה החוזרת בכל פעם שיש זמן פנוי בסוף מסגרת, או בתקופות שבהן המשתמש לא פעיל. לא תצפו שהקריאה החוזרת תתבצע לכל פריים, ואם אתם דורשים שהיא תפעל במסגרת זמן מסוימת, עליכם לנצל את הזמן הקצוב לתפוגה.
  • האם אפשר לבצע כמה קריאות חוזרות של requestIdleCallback? כן, זה יכול לקרות, עד כמה שניתן לבצע כמה קריאות חוזרות של requestAnimationFrame. עם זאת, כדאי לזכור שאם הקריאה החוזרת הראשונה תנצל את הזמן שנותר במהלך הקריאה החוזרת, לא יישאר זמן לקריאות חוזרות אחרות. הקריאות החוזרות האחרות יצטרכו להמתין עד שהדפדפן יפסיק להיות פעיל בפעם הבאה כדי שיהיה אפשר להפעיל אותן. בהתאם לעבודה שאתם מנסים לבצע, ייתכן שעדיף להשתמש בקריאה חוזרת (callback) אחת ללא פעילות ולחלק את העבודה שם. לחלופין, אפשר להשתמש בזמן הקצוב לתפוגה כדי להבטיח שקריאות חוזרות (callback) לא ייפגעו עם הזמן.
  • מה קורה אם מגדירים קריאה חוזרת (callback) חדשה ללא פעילות בתוך מכשיר אחר? הקריאה החוזרת (callback) החדשה ללא פעילות תתוזמן לפעול בהקדם האפשרי, החל מהפריים הבא (במקום במסגרת הנוכחית).

לא עכשיו!

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

כדאי לנסות את Chrome Canary, לנסות את הפרויקטים שלך ולדווח לנו על ההתקדמות שלך!