Представляем пробную версию Scheduler.yield Origin

Создание веб-сайтов, быстро реагирующих на ввод пользователя, всегда было одной из самых сложных задач в области веб-производительности — и команда Chrome усердно работала над тем, чтобы помочь веб-разработчикам решить эту проблему. Только в этом году было объявлено , что метрика Interaction to Next Paint (INP) перейдет из экспериментального статуса в статус «ожидает подтверждения». Теперь она готова заменить First Input Delay (FID) в качестве ключевого показателя веб-производительности в марте 2024 года.

В рамках постоянных усилий по предоставлению новых API, которые помогут веб-разработчикам сделать свои веб-сайты максимально быстрыми, команда Chrome в настоящее время проводит пробное тестирование функции scheduler.yield , начиная с версии 115 Chrome. scheduler.yield — это предлагаемое новое дополнение к API планировщика, которое позволяет проще и эффективнее передавать управление основному потоку, чем методы, которые традиционно использовались .

При уступке

В JavaScript используется модель выполнения до завершения для работы с задачами. Это означает, что когда задача выполняется в основном потоке, она выполняется столько времени, сколько необходимо для её завершения. После завершения задачи управление возвращается основному потоку, что позволяет основному потоку обрабатывать следующую задачу в очереди.

За исключением крайних случаев, когда задача никогда не завершается — например, бесконечный цикл — инициализация (yielding) является неизбежным аспектом логики планирования задач в JavaScript. Это произойдет , вопрос лишь в том, когда , и чем раньше, тем лучше. Когда выполнение задач занимает слишком много времени — более 50 миллисекунд, если быть точным, — они считаются длительными задачами .

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

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

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

Когда вы явно указываете на необходимость уступить место, вы говорите браузеру: «Привет, я понимаю, что работа, которую я собираюсь выполнить, может занять некоторое время, и я не хочу, чтобы вам приходилось выполнять всю эту работу, прежде чем отвечать на ввод пользователя или выполнять другие важные задачи». Это ценный инструмент в арсенале разработчика, который может значительно улучшить пользовательский опыт.

Проблема существующих стратегий получения прибыли заключается в следующем

Распространенный метод передачи задачи на выполнение использует setTimeout со значением таймаута, равным 0 Это работает потому, что функция обратного вызова, переданная в setTimeout , переместит оставшуюся работу в отдельную задачу, которая будет поставлена ​​в очередь для последующего выполнения. Вместо того чтобы ждать, пока браузер сам передаст задачу на выполнение, вы говорите: «Разбейте этот большой объем работы на более мелкие части».

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

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

  1. Нажмите верхнюю кнопку с надписью « Запускать задачи периодически» , которая запланирует выполнение блокирующих задач с определенной периодичностью. После нажатия этой кнопки в журнале задач появится несколько сообщений типа «Выполнена блокирующая задача с setInterval .
  2. Далее нажмите кнопку « Запустить цикл», при этом на каждой итерации будет устанавливаться setTimeout .

Вы заметите, что в нижней части демонстрационного ролика будет написано примерно следующее:

Processing loop item 1
Processing loop item 2
Ran blocking task via setInterval
Processing loop item 3
Ran blocking task via setInterval
Processing loop item 4
Ran blocking task via setInterval
Processing loop item 5
Ran blocking task via setInterval
Ran blocking task via setInterval

Этот вывод демонстрирует поведение «завершения очереди задач», которое происходит при использовании setTimeout для передачи управления. Цикл обрабатывает пять элементов и передает управление с помощью setTimeout после обработки каждого из них.

Это иллюстрирует распространённую проблему в интернете: нередко скрипты — особенно сторонние — регистрируют функцию таймера, которая запускает работу через определённый интервал. Поведение «конец очереди задач», возникающее при использовании оператора `yield` с setTimeout означает, что задачи из других источников могут быть поставлены в очередь раньше оставшейся работы, которую цикл должен выполнить после использования `yield`.

В зависимости от вашего приложения, такой результат может быть желательным или нежелательным, но во многих случаях именно такое поведение заставляет разработчиков неохотно отказываться от управления основным потоком. Уступка места (yielding) полезна, поскольку позволяет выполнять взаимодействие с пользователем раньше, а также даёт возможность другим задачам, не связанным с взаимодействием с пользователем, занять время в основном потоке. Это реальная проблема, но scheduler.yield может помочь её решить!

Введите scheduler.yield

Функция scheduler.yield доступна в качестве экспериментальной функции веб-платформы, скрытой за флагом, начиная с версии 115 Chrome. У вас может возникнуть вопрос: «Зачем мне специальная функция для yield, если setTimeout уже это делает?»

Стоит отметить, что использование `yield` не было целью проектирования ` setTimeout , а скорее приятным побочным эффектом при планировании выполнения обратного вызова в более поздний момент времени — даже при указании значения таймаута, равного 0 Однако важнее помнить, что использование `yield` с setTimeout отправляет оставшуюся работу в конец очереди задач. По умолчанию ` scheduler.yield отправляет оставшуюся работу в начало очереди. Это означает, что работа, которую вы хотели возобновить сразу после использования `yield`, не будет отложена по сравнению с задачами из других источников (за заметным исключением взаимодействий с пользователем).

Функция scheduler.yield передает управление основному потоку и возвращает ` Promise при вызове. Это означает, что вы можете await в async функции:

async function yieldy () {
  // Do some work...
  // ...

  // Yield!
  await scheduler.yield();

  // Do some more work...
  // ...
}

Чтобы увидеть работу scheduler.yield , выполните следующие действия:

  1. Перейдите по адресу chrome://flags .
  2. Включите экспериментальную версию функций экспериментальной веб-платформы . Возможно, после этого потребуется перезапустить Chrome.
  3. Перейдите на демонстрационную страницу или воспользуйтесь встроенной версией, расположенной после этого списка.
  4. Нажмите верхнюю кнопку с надписью « Периодически запускать задачи» .
  5. Наконец, нажмите кнопку « Запустить цикл», передавая управление с помощью scheduler.yield на каждой итерации .

Результат в поле внизу страницы будет выглядеть примерно так:

Processing loop item 1
Processing loop item 2
Processing loop item 3
Processing loop item 4
Processing loop item 5
Ran blocking task via setInterval
Ran blocking task via setInterval
Ran blocking task via setInterval
Ran blocking task via setInterval
Ran blocking task via setInterval

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

Попробуйте!

Если вам показалась интересной scheduler.yield , и вы хотите её попробовать, вы можете сделать это двумя способами, начиная с версии 115 Chrome:

  1. Если вы хотите поэкспериментировать с scheduler.yield локально, введите chrome://flags в адресной строке Chrome и выберите «Включить» в раскрывающемся списке в разделе « Экспериментальные функции веб-платформы» . Это сделает scheduler.yield (и любые другие экспериментальные функции) доступными только в вашем экземпляре Chrome.
  2. Если вы хотите включить scheduler.yield для реальных пользователей Chromium на общедоступном сервере, вам необходимо зарегистрироваться для участия в пробной версии scheduler.yield . Это позволит вам безопасно экспериментировать с предлагаемыми функциями в течение определенного периода времени и предоставит команде Chrome ценную информацию о том, как эти функции используются на практике. Для получения дополнительной информации о том, как работают пробные версии, ознакомьтесь с этим руководством .

Способ использования scheduler.yield — при сохранении поддержки браузеров, которые его не реализуют — зависит от ваших целей. Вы можете использовать официальный полифил . Полифил полезен, если к вашей ситуации применимы следующие условия:

  1. В вашем приложении уже используется scheduler.postTask для планирования задач.
  2. Вам необходимо иметь возможность устанавливать приоритеты задач и результатов.
  3. Вам нужна возможность отменять или изменять приоритет задач с помощью класса TaskController , который предоставляет API scheduler.postTask .

Если это не описывает вашу ситуацию, то полифил, возможно, вам не подойдёт. В этом случае вы можете создать собственный резервный вариант несколькими способами. Первый подход использует scheduler.yield , если он доступен, но в случае отсутствия возможности использует setTimeout :

// A function for shimming scheduler.yield and setTimeout:
function yieldToMain () {
  // Use scheduler.yield if it exists:
  if ('scheduler' in window && 'yield' in scheduler) {
    return scheduler.yield();
  }

  // Fall back to setTimeout:
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  });
}

// Example usage:
async function doWork () {
  // Do some work:
  // ...

  await yieldToMain();

  // Do some other work:
  // ...
}

Это может сработать, но, как вы можете догадаться, браузеры, которые не поддерживают scheduler.yield , будут передавать управление без поведения "в начале очереди". Если это означает, что вы предпочитаете вообще не передавать управление, вы можете попробовать другой подход, который использует scheduler.yield , если он доступен, но не будет передавать управление вообще, если он недоступен:

// A function for shimming scheduler.yield with no fallback:
function yieldToMain () {
  // Use scheduler.yield if it exists:
  if ('scheduler' in window && 'yield' in scheduler) {
    return scheduler.yield();
  }

  // Fall back to nothing:
  return;
}

// Example usage:
async function doWork () {
  // Do some work:
  // ...

  await yieldToMain();

  // Do some other work:
  // ...
}

scheduler.yield — это интересное дополнение к API планировщика, которое, как мы надеемся, упростит разработчикам повышение скорости отклика по сравнению с существующими стратегиями перераспределения ресурсов. Если scheduler.yield кажется вам полезным API, пожалуйста, примите участие в наших исследованиях, чтобы помочь улучшить его, и предоставьте отзывы о том, как его можно进一步 усовершенствовать.

Главное изображение взято с Unsplash , автор — Джонатан Эллисон .