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

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

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

Об уступке

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

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

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

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

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

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

Проблема с текущими стратегиями доходности

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

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

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

  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 после обработки каждого из них.

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

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

Введите scheduler.yield

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

Стоит отметить, что передача не была целью разработки setTimeout , а скорее приятным побочным эффектом при планировании обратного вызова для запуска в более поздний момент в будущем — даже с указанным значением таймаута, равным 0 . Однако более важно помнить, что передача с помощью setTimeout отправляет оставшуюся работу в конец очереди задач. По умолчанию scheduler.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 Origin . Это позволяет вам безопасно экспериментировать с предлагаемыми функциями в течение определенного периода времени и дает команде 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 , автор Джонатан Эллисон .