Использование запросаIdleCallback

Многие сайты и приложения содержат множество скриптов для выполнения. Ваш JavaScript часто необходимо запустить как можно скорее, но в то же время вы не хотите, чтобы он мешал пользователю. Если вы отправляете аналитические данные, когда пользователь прокручивает страницу, или добавляете элементы в DOM, когда они нажимают на кнопку, ваше веб-приложение может перестать отвечать на запросы, что приведет к ухудшению пользовательского опыта.

Использование requestIdleCallback для планирования второстепенной работы.

Хорошей новостью является то, что теперь есть API, который может помочь: requestIdleCallback . Точно так же, как внедрение requestAnimationFrame позволило нам правильно планировать анимацию и максимизировать шансы на достижение 60 кадров в секунду, requestIdleCallback будет планировать работу, когда в конце кадра есть свободное время или когда пользователь неактивен. Это означает, что есть возможность выполнять свою работу, не мешая пользователю. Он доступен начиная с Chrome 47, так что вы можете опробовать его уже сегодня, используя Chrome Canary! Это экспериментальная функция , и ее спецификация все еще меняется, поэтому в будущем все может измениться.

Почему мне следует использовать requestIdleCallback?

Самостоятельно планировать второстепенную работу очень сложно. Невозможно точно определить, сколько времени кадра осталось, потому что после выполнения обратных вызовов requestAnimationFrame необходимо выполнить вычисления стиля, макета, рисования и другие внутренние функции браузера. Домашнее решение не может учитывать ни один из этих факторов. Чтобы быть уверенным, что пользователь каким-либо образом не взаимодействует, вам также необходимо прикрепить прослушиватели к каждому виду событий взаимодействия ( scroll , touch , click ), даже если они вам не нужны для функциональности, просто чтобы вы можете быть абсолютно уверены, что пользователь не взаимодействует. Браузер, с другой стороны, точно знает, сколько времени доступно в конце кадра и взаимодействует ли пользователь, поэтому с помощью requestIdleCallback мы получаем API, который позволяет нам использовать любое свободное время с максимальной пользой. возможен эффективный способ.

Давайте рассмотрим его более подробно и посмотрим, как мы можем его использовать.

Проверка запросаIdleCallback

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 был недоступен, вам не будет хуже, если вы подстроитесь таким образом. С помощью прокладки, если requestIdleCallback доступен, ваши вызовы будут автоматически перенаправляться, и это здорово.

А пока давайте предположим, что он существует.

Использование запросаIdleCallback

Вызов 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 });

Если ваш обратный вызов выполняется из-за таймаута, вы заметите две вещи:

  • 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 внутри обратного вызова простоя, эти вычисления макета будут признаны недействительными. Если в следующем кадре происходит чтение какого-либо макета, например getBoundingClientRect , clientWidth и т. д., браузеру придется выполнить принудительное синхронное размещение , что является потенциальным узким местом производительности.

Другая причина, по которой не следует запускать изменения DOM в обратном вызове простоя, заключается в том, что временные последствия изменения DOM непредсказуемы, и поэтому мы можем легко превысить крайний срок, предоставленный браузером.

Лучше всего вносить изменения в DOM только внутри обратного вызова requestAnimationFrame , поскольку он запланирован браузером с учетом этого типа работы. Это означает, что нашему коду потребуется использовать фрагмент документа, который затем можно будет добавить в следующий обратный вызов 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 , который установит один обратный вызов requestAnimationFrame , который, в свою очередь, добавит фрагмент документа в тело:

function scheduleVisualUpdateIfNeeded() {

    if (isVisualUpdateScheduled)
    return;

    isVisualUpdateScheduled = true;

    requestAnimationFrame(appendDocumentFragment);
}

function appendDocumentFragment() {
    // Append the fragment and reset.
    document.body.appendChild(documentFragment);
    documentFragment = null;
}

Если все будет хорошо, мы теперь будем видеть гораздо меньше ошибок при добавлении элементов в DOM. Отличный!

Часто задаваемые вопросы

  • Есть ли полифил? К сожалению, нет, но есть прокладка, если вы хотите иметь прозрачное перенаправление на setTimeout . Причина существования этого API заключается в том, что он закрывает вполне реальный пробел в веб-платформе. Сделать вывод об отсутствии активности сложно, но не существует API-интерфейсов JavaScript для определения количества свободного времени в конце кадра, поэтому в лучшем случае приходится строить догадки. Такие API, как setTimeout , setInterval или setImmediate , можно использовать для планирования работы, но они не рассчитаны по времени, чтобы избежать взаимодействия с пользователем, как это происходит с requestIdleCallback .
  • Что произойдет, если я просрочу срок? Если timeRemaining() возвращает ноль, но вы решили работать дольше, вы можете делать это, не опасаясь, что браузер остановит вашу работу. Тем не менее, браузер дает вам крайний срок, чтобы попытаться обеспечить бесперебойную работу ваших пользователей, поэтому, если нет веских причин, вы всегда должны придерживаться этого срока.
  • Существует ли максимальное значение, которое вернет timeRemaining() ? Да, сейчас это 50 мс. При попытке поддерживать быстродействующее приложение все ответы на действия пользователя не должны превышать 100 мс. Если пользователь взаимодействует, окно 50 мс в большинстве случаев должно позволить завершить обратный вызов бездействия и дать возможность браузеру отреагировать на взаимодействие пользователя. Вы можете запланировать несколько обратных вызовов в режиме ожидания один за другим (если браузер определит, что для их запуска достаточно времени).
  • Есть ли какая-то работа, которую мне не следует выполнять в requestIdleCallback? В идеале выполняемая вами работа должна состоять из небольших частей (микрозадач) с относительно предсказуемыми характеристиками. Например, изменение DOM, в частности, будет иметь непредсказуемое время выполнения, поскольку оно вызовет расчеты стиля, макетирование, рисование и композицию. Таким образом, вам следует вносить изменения в DOM только в обратном вызове requestAnimationFrame , как предложено выше. Еще одна вещь, которой следует опасаться, — это разрешение (или отклонение) промисов, поскольку обратные вызовы будут выполняться сразу после завершения обратного вызова в режиме ожидания, даже если времени больше не осталось.
  • Всегда ли я буду получать requestIdleCallback в конце кадра? Нет, не всегда. Браузер запланирует обратный вызов всякий раз, когда в конце кадра появляется свободное время или в периоды, когда пользователь неактивен. Не следует ожидать, что обратный вызов будет вызываться для каждого кадра, и если вам требуется, чтобы он выполнялся в течение заданного периода времени, вам следует использовать тайм-аут.
  • Могу ли я иметь несколько обратных вызовов requestIdleCallback ? Да, вы можете, так же как и иметь несколько обратных вызовов requestAnimationFrame . Однако стоит помнить, что если ваш первый обратный вызов израсходует время, оставшееся во время обратного вызова, то для других обратных вызовов времени больше не останется. Другим обратным вызовам придется ждать, пока браузер не перейдет в режим ожидания, прежде чем их можно будет запустить. В зависимости от работы, которую вы пытаетесь выполнить, возможно, лучше иметь один простой обратный вызов и разделить работу на него. В качестве альтернативы вы можете использовать тайм-аут, чтобы гарантировать, что обратные вызовы не будут испытывать недостатка во времени.
  • Что произойдет, если я установлю новый обратный вызов в режиме ожидания внутри другого? Новый обратный вызов при простое будет запланирован на запуск как можно скорее, начиная со следующего кадра (а не текущего).

Без дела!

requestIdleCallback — это отличный способ убедиться, что вы можете запустить свой код, не мешая при этом пользователю. Он прост в использовании и очень гибок. Однако еще рано, и спецификация еще не полностью согласована, поэтому любые ваши отзывы приветствуются.

Попробуйте его в Chrome Canary, попробуйте в своих проектах и ​​дайте нам знать, как у вас дела!