Создание веб-сайтов, которые быстро реагируют на пользовательский ввод, было одним из самых сложных аспектов веб-производительности — тем, над которым команда Chrome упорно трудилась, чтобы помочь веб-разработчикам справиться. Только в этом году было объявлено , что метрика Interaction to Next Paint (INP) перейдет из экспериментального в статус ожидания. Теперь она готова заменить First Input Delay (FID) в качестве Core Web Vital в марте 2024 года.
Продолжая работу над новыми API, которые помогут веб-разработчикам сделать свои веб-сайты максимально быстрыми, команда Chrome в настоящее время запускает пробную версию scheduler.yield
, начиная с версии 115 Chrome. scheduler.yield
— это предлагаемое новое дополнение к API планировщика, которое обеспечивает более простой и эффективный способ передачи управления обратно основному потоку, чем методы, на которые традиционно полагались .
На уступке
JavaScript использует модель выполнения до завершения для работы с задачами. Это означает, что когда задача выполняется в основном потоке, она выполняется столько времени, сколько необходимо для завершения. После завершения задачи управление возвращается основному потоку, что позволяет основному потоку обрабатывать следующую задачу в очереди.
За исключением крайних случаев, когда задача никогда не завершается — например, бесконечный цикл — yielding является неизбежным аспектом логики планирования задач JavaScript. Это произойдет , это просто вопрос времени , и лучше раньше, чем позже. Когда задачи выполняются слишком долго — более 50 миллисекунд, если быть точным — они считаются длинными задачами .
Длительные задачи являются источником плохой отзывчивости страницы, поскольку они задерживают способность браузера реагировать на пользовательский ввод. Чем чаще возникают длительные задачи — и чем дольше они выполняются — тем больше вероятность того, что у пользователей может сложиться впечатление, что страница работает медленно или даже что она вообще сломана.
Однако, только потому, что ваш код запускает задачу в браузере, не означает, что вам нужно ждать, пока эта задача не будет завершена, прежде чем управление будет передано обратно основному потоку. Вы можете улучшить отзывчивость к пользовательскому вводу на странице, явно уступив в задаче, что разбивает задачу на части, чтобы завершить ее при первой же возможности. Это позволяет другим задачам получить время в основном потоке раньше, чем если бы им пришлось ждать завершения длинных задач.

Когда вы явно уступаете, вы говорите браузеру: «Эй, я понимаю, что работа, которую я собираюсь сделать, может занять некоторое время, и я не хочу, чтобы вам пришлось делать всю эту работу, прежде чем вы ответите на пользовательский ввод или другие задачи, которые также могут быть важны». Это ценный инструмент в арсенале разработчика, который может значительно улучшить пользовательский опыт.
Проблема с текущими стратегиями доходности
Обычный метод yielding использует setTimeout
со значением таймаута 0
. Это работает, потому что обратный вызов, переданный setTimeout
, переместит оставшуюся работу в отдельную задачу, которая будет поставлена в очередь для последующего выполнения. Вместо того чтобы ждать, пока браузер сам уступит, вы говорите: «Давайте разобьем этот большой кусок работы на более мелкие части».
Однако yielding с setTimeout
несет в себе потенциально нежелательный побочный эффект: работа, которая идет после точки yield, будет перемещена в конец очереди задач. Задачи, запланированные взаимодействием с пользователем, по-прежнему будут перемещаться в начало очереди, как и должны, но оставшаяся работа, которую вы хотели сделать после явного yielding, может оказаться еще более задержанной другими задачами из конкурирующих источников, которые были поставлены в очередь перед ней.
Чтобы увидеть это в действии, попробуйте эту демо-версию Glitch — или поэкспериментируйте с ней во встроенной версии ниже. Демо-версия состоит из нескольких кнопок, которые можно нажать, и поля под ними, которое регистрирует выполнение задач. Когда вы попадаете на страницу, выполните следующие действия:
- Нажмите верхнюю кнопку с надписью Запускать задачи периодически , что запланирует запуск блокирующих задач с определенной частотой. Когда вы нажмете эту кнопку, журнал задач заполнится несколькими сообщениями, которые будут звучать как Запустил блокирующую задачу с
setInterval
. - Затем нажмите кнопку с надписью Запустить цикл, выдавая
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
после обработки каждого из них.
Это иллюстрирует распространенную проблему в Интернете: для скрипта, особенно стороннего скрипта, не является чем-то необычным регистрировать функцию таймера, которая запускает работу с некоторым интервалом. Поведение «конца очереди задач», которое возникает при yielding с setTimeout
означает, что работа из других источников задач может быть поставлена в очередь раньше оставшейся работы, которую цикл должен выполнить после yielding.
В зависимости от вашего приложения это может быть желаемым результатом, но во многих случаях именно это поведение является причиной того, что разработчики могут неохотно отказываться от контроля над основным потоком так легко. Уступка хороша тем, что пользовательские взаимодействия имеют возможность запускаться раньше, но она также позволяет другой работе, не связанной с пользовательским взаимодействием, также получить время в основном потоке. Это реальная проблема, но scheduler.yield
может помочь решить ее!
Введите scheduler.yield
scheduler.yield
доступен за флагом как экспериментальная функция веб-платформы с версии 115 Chrome. Один из вопросов, который у вас может возникнуть: «зачем мне нужна специальная функция для yield, если setTimeout
уже делает это?»
Стоит отметить, что yielding не был целью разработки setTimeout
, а скорее приятным побочным эффектом при планировании обратного вызова для запуска в более поздний момент в будущем — даже при указанном значении тайм-аута 0
Однако важнее помнить, что yielding с setTimeout
отправляет оставшуюся работу в конец очереди задач. По умолчанию scheduler.yield
отправляет оставшуюся работу в начало очереди. Это означает, что работа, которую вы хотели возобновить немедленно после yielding, не будет отодвинута на второй план задачами из других источников (за заметным исключением взаимодействия с пользователем).
scheduler.yield
— это функция, которая уступает основному потоку и возвращает Promise
при вызове. Это означает, что вы можете await
его в async
функции:
async function yieldy () {
// Do some work...
// ...
// Yield!
await scheduler.yield();
// Do some more work...
// ...
}
Чтобы увидеть scheduler.yield
в действии, выполните следующие действия:
- Перейдите по адресу
chrome://flags
. - Включите эксперимент с функциями экспериментальной веб-платформы . После этого вам, возможно, придется перезапустить Chrome.
- Перейдите на демонстрационную страницу или используйте ее встроенную версию под этим списком.
- Нажмите верхнюю кнопку с надписью Периодически запускать задачи .
- Наконец, нажмите кнопку с надписью Запустить цикл, выдавая
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
В отличие от демонстрации, которая yields использует setTimeout
, вы можете видеть, что цикл — даже если он yields после каждой итерации — не отправляет оставшуюся работу в конец очереди, а скорее в ее начало. Это дает вам лучшее из обоих миров: вы можете yield улучшить отзывчивость ввода на вашем сайте, но также гарантировать, что работа, которую вы хотели завершить после yielding, не будет отложена.
Попробуйте!
Если scheduler.yield
показался вам интересным и вы хотите его опробовать, это можно сделать двумя способами, начиная с версии 115 Chrome:
- Если вы хотите поэкспериментировать с
scheduler.yield
локально, введитеchrome://flags
в адресной строке Chrome и выберите Включить из раскрывающегося списка в разделе Экспериментальные функции веб-платформы . Это сделаетscheduler.yield
(и любые другие экспериментальные функции) доступными только в вашем экземпляре Chrome. - Если вы хотите включить
scheduler.yield
для реальных пользователей Chromium в общедоступном источнике, вам нужно будет зарегистрироваться для пробной версииscheduler.yield
origin . Это позволит вам безопасно экспериментировать с предлагаемыми функциями в течение определенного периода времени и предоставит команде Chrome ценную информацию о том, как эти функции используются в полевых условиях. Для получения дополнительной информации о том, как работают пробные версии источника, прочтите это руководство .
То, как вы используете scheduler.yield
— при этом поддерживая браузеры, которые его не реализуют — зависит от ваших целей. Вы можете использовать официальный полифилл . Полифилл полезен, если к вашей ситуации применимо следующее:
- Вы уже используете
scheduler.postTask
в своем приложении для планирования задач. - Вы хотите уметь ставить задачи и расставлять приоритеты.
- Вы хотите иметь возможность отменять или изменять приоритет задач с помощью класса
TaskController
, предлагаемого APIscheduler.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
, будут выдавать данные без поведения "front of queue". Если это означает, что вы предпочли бы вообще не выдавать данные, вы можете попробовать другой подход, который использует 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 планировщика, которое, как мы надеемся, облегчит разработчикам задачу улучшения отзывчивости по сравнению с текущими стратегиями yielding. Если scheduler.yield
кажется вам полезным API, примите участие в нашем исследовании, чтобы помочь улучшить его, и предоставьте отзыв о том, как его можно улучшить.
Главное изображение из Unsplash , автор Джонатан Эллисон .