Создание веб-сайтов, которые быстро реагируют на действия пользователя, было одним из самых сложных аспектов веб-производительности, над решением которого команда Chrome усердно работала, чтобы помочь веб-разработчикам. Только в этом году было объявлено , что метрика «Взаимодействие с следующей отрисовкой» (INP) перейдет из экспериментального статуса в статус ожидания. Теперь он готов заменить First Input Delay (FID) в качестве Core Web Vital в марте 2024 года.
Продолжая прилагать усилия по созданию новых API, которые помогут веб-разработчикам сделать свои веб-сайты максимально быстрыми, команда Chrome в настоящее время запускает пробную версию scheduler.yield
начиная с версии 115 Chrome. scheduler.yield
— это предлагаемое новое дополнение к API планировщика, которое обеспечивает более простой и лучший способ вернуть управление основному потоку, чем методы, на которые традиционно полагались .
Об уступке
JavaScript использует модель выполнения до завершения для решения задач. Это означает, что когда задача выполняется в основном потоке, она выполняется столько времени, сколько необходимо для ее завершения. После завершения задачи управление возвращается основному потоку, что позволяет основному потоку обработать следующую задачу в очереди.
За исключением крайних случаев, когда задача никогда не завершается (например, бесконечный цикл), выдача является неизбежным аспектом логики планирования задач JavaScript. Это произойдет , это всего лишь вопрос времени , и лучше раньше, чем позже. Когда задачи выполняются слишком долго (точнее, более 50 миллисекунд), они считаются длинными задачами .
Длинные задачи являются источником плохой реакции страницы, поскольку они задерживают способность браузера реагировать на ввод пользователя. Чем чаще возникают длительные задачи — и чем дольше они выполняются — тем больше вероятность того, что у пользователей может сложиться впечатление, что страница работает медленно, или даже почувствовать, что она вообще не работает.
Однако тот факт, что ваш код запускает задачу в браузере, не означает, что вам нужно ждать завершения этой задачи, прежде чем управление будет возвращено основному потоку. Вы можете улучшить реагирование на ввод пользователя на странице, явно уступив задачу, что разбивает задачу на завершение при следующей доступной возможности. Это позволяет другим задачам получить время в основном потоке раньше, чем если бы им приходилось ждать завершения длительных задач.
Когда вы явно уступаете, вы говорите браузеру: «Эй, я понимаю, что работа, которую я собираюсь сделать, может занять некоторое время, и я не хочу, чтобы вам приходилось делать всю эту работу, прежде чем ответить на ввод пользователя. или другие задачи, которые тоже могут быть важными». Это ценный инструмент в наборе инструментов разработчика, который может существенно улучшить взаимодействие с пользователем.
Проблема с текущими стратегиями доходности
Распространенный метод передачи использует setTimeout
со значением таймаута 0
. Это работает, поскольку обратный вызов, переданный в setTimeout
переместит оставшуюся работу в отдельную задачу, которая будет поставлена в очередь для последующего выполнения. Вместо того, чтобы ждать, пока браузер выполнит свои действия самостоятельно, вы говорите: «Давайте разобьем этот большой кусок работы на более мелкие части».
Однако передача с помощью setTimeout
имеет потенциально нежелательный побочный эффект: работа, которая происходит после точки возврата, будет отправлена в конец очереди задач. Задачи, запланированные взаимодействием с пользователем, по-прежнему будут помещаться в начало очереди, как и должны, но оставшаяся работа, которую вы хотели выполнить после явного отказа, может в конечном итоге быть еще более задержана другими задачами из конкурирующих источников, которые были поставлены в очередь перед ней.
Чтобы увидеть это в действии, попробуйте эту демонстрацию 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
после обработки каждого из них.
Это иллюстрирует распространенную проблему в Интернете: сценарий, особенно сторонний, нередко регистрирует функцию таймера, которая выполняет работу через определенный интервал. Поведение «конец очереди задач», возникающее при передаче с помощью 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
в действии, сделайте следующее:
- Перейдите по адресу
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
В отличие от демонстрации, которая дает результат с использованием setTimeout
, вы можете видеть, что цикл — даже несмотря на то, что он завершается после каждой итерации — отправляет оставшуюся работу не в конец очереди, а в ее начало. Это дает вам лучшее из обоих миров: вы можете улучшить скорость реагирования на ввод на своем веб-сайте, а также гарантировать, что работа, которую вы хотели завершить после передачи, не задерживается.
Попробуйте!
Если 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
будут работать без поведения «впереди очереди». Если это означает, что вы вообще не хотите давать доход, вы можете попробовать другой подход, который использует 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 , автор Джонатан Эллисон .