Как распланировать фоновые задачи при помощи Javascript

В браузере код JavaScript запускается на выполнение сразу после обработки разметки страницы. Код скачивается (если это необходимо) и запускается еще до отображения содержимого веб-страницы. Это важно, так как ваш скрипт может отвечать за выполнение различных функций: загружать последующий код, удалять каждый DOM-элемент, перенаправлять посетителя на другой URL и т. д. Но это блокирует дальнейшую загрузку страницы, и она перестает отвечать на запросы.

Часто требуется, чтобы JavaScript-код был запущен как можно скорее, так как он инициализирует виджеты и контроллеры событий. Но есть множество менее важных фоновых задач, остановка которых никак не сказываются на взаимодействии ресурса с пользователем:

  • Считывание данных аналитики;
  • Отправка данных в социальные сети;
  • Предварительное извлечение контента, находящегося за пределами окна просмотра;
  • Предварительная обработка и вывод HTML-элементов.

Эти задачи влияют на скорость загрузки несущественно. Но для того, чтобы страница сохраняла свои адаптивные свойства, не стоит запускать эти задачи во время взаимодействия пользователя с контентом или прокручивания им страницы.

Решение этой проблемы очень простое – использовать Web Workers – объекты, которые способны обрабатывать код отдельным потоком. Они отлично подходят для предварительного извлечения и обработки, но с их помощью вы не сможете напрямую осуществлять доступ или обновлять DOM. В своих собственных скриптах вам это может и не понадобиться, но нет никакой гарантии, что это не потребуется сторонним скриптам вроде Google Analytics.

Есть еще одна опция – использовать setTimeout (или setTimeout(doSomething, 1). Браузер выполнит функцию doSomething() лишь после того, как будут завершены другие, более важные задачи. В результате запуск функции получает самое последнее место в очереди. При этом следует учитывать, что функция будет запущена в любом случае.

requestIdleCallback

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

Поддержку requestIdleCallback можно определить следующим образом:

if ('requestIdleCallback' in window) {
  // requestIdleCallback supported
  requestIdleCallback(backgroundTask);
}
else {
  // no support - do something else
  setTimeout(backgroundTask1, 1);
  setTimeout(backgroundTask2, 1);
  setTimeout(backgroundTask3, 1);
}

Также можно задать значение для параметра timeout (в миллисекундах):

requestIdleCallback(backgroundTask, { timeout: 3000; });

Такой запрос гарантирует, что ваша функция будет запущена через три секунды, независимо от того, на каком этапе загрузки находится браузер.

requestIdleCallback лишь один раз запускает вашу функцию и передает объект deadline со следующими параметрами:

  • didTimeout — используйте значение true, если вам нужно включить таймаут;
  • timeRemaining() — функция, которая возвращает количество миллисекунд, оставшихся до запуска задачи;
  • timeRemaining() допускает не более 50 миллисекунд на выполнение вашей задачи. Конечно, она не будет останавливать задачи, превышающие этот лимит, но желательно снова запустить requestIdleCallback для продолжения выполнения задачи.

Давайте создадим простой пример, в котором по очереди будут выполняться несколько задач. Задачи хранятся в массиве в виде ссылок на функции:

// Массив функций для запуска
var task = [
	background1,
	background2,
	background3
];

if ('requestIdleCallback' in window) {
  // Проверка поддержки requestIdleCallback
  requestIdleCallback(backgroundTask);
}
else {
  // Если не поддерживается, то установка таймаута запуска задачи
  while (task.length) {
  	setTimeout(task.shift(), 1);
  }
}

// Функция обратного вызова requestIdleCallback
function backgroundTask(deadline) {

  // Если возможно, то запускаются на выполнение следующие задания
  while (deadline.timeRemaining() > 0 && task.length > 0) {
  	task.shift()();
  }

  // Запланировать выполнение следующих задач
  if (task.length > 0) {
    requestIdleCallback(backgroundTask);
  }
}

Есть ли что-нибудь, что не следует использовать в requestIdleCallback?

Работа с requestIdleCallback должна осуществляться небольшими фрагментами. Этот способ не подходит для случаев, когда неизвестно время, необходимое для выполнения. Например, работа с DOM, которую лучше реализовать при помощи callback-функции requestAnimationFrame. Также нужно быть осторожным с разрешением или отказом при работе с объектом Promises, так как callback-функции будут запущены сразу после того, как будет завершена функция idle.

Браузерная поддержка requestIdleCallback

requestIdleCallback – это экспериментальное свойство, и его спецификация до сих пор находится в стадии доработки, поэтому не удивляйтесь, если столкнетесь с изменениями в API. Это свойство поддерживается в Google Chrome 47. Также внедрение соответствующей поддержки ожидалось и в Opera. Microsoft и Mozilla присматриваются к этому свойству, так как оно дает определенные надежды. Как обычно, нет никакой информации от компании Apple. Если вам все же захочется использовать данную функцию, рекомендую воспользоваться Chrome Canary.

Разработан простенький шаблон requestIdleCallback. Он запускает API, но он не способен эмулировать браузерные механизмы определения простоев.

Хотя на сегодняшний день поддержка сильно ограничена, requestIdleCallback может стать отличной опцией, которая позволит улучшить производительность веб-страниц.

Перевод статьи “How to Schedule Background Tasks in JavaScript” был подготовлен дружной командой проекта Сайтостроение от А до Я.