שימוש ב-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, האפשרות 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, להתנסות בפרויקטים שלכם, לספר לנו איך אתם מתקדמים.