Опубликовано: 6 марта 2025 г.
Страница кажется вялой и неотзывчивой, когда длительные задачи занимают основной поток, не давая ему выполнять другую важную работу, например, реагировать на пользовательский ввод. В результате даже встроенные элементы управления формами могут показаться пользователям сломанными — как будто страница заморожена — не говоря уже о более сложных пользовательских компонентах.
scheduler.yield()
— это способ уступить главному потоку, позволяя браузеру выполнить любую отложенную высокоприоритетную работу, а затем продолжить выполнение с того места, где оно остановилось. Это сохраняет страницу более отзывчивой и, в свою очередь, помогает улучшить взаимодействие до следующей отрисовки (INP) .
scheduler.yield
предлагает эргономичный API, который делает ровно то, что и заявлено: выполнение функции, в которой она вызывается, останавливается на выражении await scheduler.yield()
и уступает основному потоку, прерывая задачу. Выполнение остальной части функции — называемой продолжением функции — будет запланировано для запуска в новой задаче цикла событий.
async function respondToUserClick() {
giveImmediateFeedback();
await scheduler.yield(); // Yield to the main thread.
slowerComputation();
}
Конкретное преимущество scheduler.yield
заключается в том, что продолжение после yield запланировано для запуска до запуска любых других подобных задач, поставленных в очередь страницей. Он отдает приоритет продолжению задачи перед запуском новых задач.
Такие функции, как setTimeout
или scheduler.postTask
также можно использовать для разделения задач, но эти продолжения обычно запускаются после любых уже поставленных в очередь новых задач, что может привести к длительным задержкам между передачей управления основному потоку и завершением работы.
Приоритетные продолжения после уступки
scheduler.yield
является частью API приоритетного планирования задач . Как веб-разработчики, мы обычно не говорим о порядке, в котором цикл событий запускает задачи с точки зрения явных приоритетов, но относительные приоритеты всегда есть , например, обратный вызов requestIdleCallback
, запускаемый после любых поставленных в очередь обратных вызовов setTimeout
, или сработавший прослушиватель событий ввода, обычно запускаемый перед задачей, поставленной в очередь с помощью setTimeout(callback, 0)
.
Планирование приоритетных задач просто делает это более явным, облегчая определение того, какая задача будет выполняться раньше другой, и позволяет корректировать приоритеты для изменения порядка выполнения при необходимости.
Как уже упоминалось, продолжение выполнения функции после yield с scheduler.yield()
получает более высокий приоритет, чем запуск других задач. Руководящая концепция заключается в том, что продолжение задачи должно выполняться первым, прежде чем переходить к другим задачам. Если задача представляет собой хорошо себя ведущий код, который периодически yield, чтобы браузер мог выполнять другие важные вещи (например, реагировать на ввод пользователя), ее не следует наказывать за yield, получая приоритет после других подобных задач.
Вот пример: две функции, поставленные в очередь для выполнения в разных задачах с помощью setTimeout
.
setTimeout(myJob);
setTimeout(someoneElsesJob);
В этом случае два вызова setTimeout
находятся прямо рядом друг с другом, но на реальной странице они могут быть вызваны в совершенно разных местах, например, сторонний скрипт и сторонний скрипт, независимо настраивающие работу для запуска, или это могут быть две задачи из отдельных компонентов, запускаемые глубоко в планировщике вашего фреймворка.
Вот как эта работа может выглядеть в DevTools:
myJob
помечен как длительная задача, блокирующая браузер от выполнения чего-либо еще во время его выполнения. Предполагая, что это скрипт первой стороны, мы можем разбить его:
function myJob() {
// Run part 1.
myJobPart1();
// Yield with setTimeout() to break up long task, then run part2.
setTimeout(myJobPart2, 0);
}
Поскольку myJobPart2
был запланирован для запуска с setTimeout
внутри myJob
, но это планирование выполняется после того, как someoneElsesJob
уже был запланирован, вот как будет выглядеть выполнение:
Мы разбили задачу с помощью setTimeout
, чтобы браузер мог реагировать в середине myJob
, но теперь вторая часть myJob
запускается только после завершения someoneElsesJob
.
В некоторых случаях это может быть нормально, но обычно это не оптимально. myJob
уступал основному потоку, чтобы убедиться, что страница может оставаться отзывчивой на ввод пользователя, а не полностью отказываться от основного потока. В случаях, когда someoneElsesJob
особенно медленный или было запланировано много других заданий, помимо someoneElsesJob
, может пройти много времени, прежде чем будет запущена вторая половина myJob
. Вероятно, это не было намерением разработчика, когда он добавлял этот setTimeout
в myJob
.
Введите scheduler.yield()
, который помещает продолжение любой функции, вызывающей его, в очередь с немного более высоким приоритетом, чем запуск любых других подобных задач. Если myJob
изменен для его использования:
async function myJob() {
// Run part 1.
myJobPart1();
// Yield with scheduler.yield() to break up long task, then run part2.
await scheduler.yield();
myJobPart2();
}
Теперь исполнение выглядит так:
Браузер по-прежнему имеет возможность быть отзывчивым, но теперь продолжение задачи myJob
имеет приоритет над началом новой задачи someoneElsesJob
, поэтому myJob
завершается до начала someoneElsesJob
. Это гораздо ближе к ожиданию уступки основному потоку для сохранения отзывчивости, а не к отказу от основного потока полностью.
Приоритетное наследование
Как часть более крупного API приоритетного планирования задач, scheduler.yield()
хорошо сочетается с явными приоритетами, доступными в scheduler.postTask()
. Без явно заданного приоритета scheduler.yield()
в обратном вызове scheduler.postTask()
будет действовать в основном так же, как и в предыдущем примере.
Однако если установлен приоритет, например, используется низкий 'background'
приоритет:
async function lowPriorityJob() {
part1();
await scheduler.yield();
part2();
}
scheduler.postTask(lowPriorityJob, {priority: 'background'});
Продолжение будет запланировано с приоритетом, который выше, чем у других 'background'
задач — получение ожидаемого приоритетного продолжения до любой ожидающей 'background'
работы — но все равно с более низким приоритетом, чем у других задач по умолчанию или задач с высоким приоритетом; оно остается 'background'
работой.
Это означает, что если вы запланируете низкоприоритетную работу с помощью 'background'
scheduler.postTask()
(или с помощью requestIdleCallback
), продолжение после scheduler.yield()
также будет ждать, пока большинство других задач не будут завершены, а основной поток не будет готов к выполнению, а это именно то, чего вы ждете от yield в низкоприоритетной работе.
Как использовать API
На данный момент scheduler.yield()
доступен только в браузерах на базе Chromium, поэтому для его использования вам потребуется определить функцию и перейти на дополнительный способ передачи данных для других браузеров.
scheduler-polyfill
— это небольшой полифилл для scheduler.postTask
и scheduler.yield
, который внутренне использует комбинацию методов для эмуляции большей части возможностей API планирования в других браузерах (хотя наследование приоритетов scheduler.yield()
не поддерживается).
Для тех, кто хочет избежать полифилла, один из методов — уступить с помощью setTimeout()
и принять потерю приоритетного продолжения или даже не уступать в неподдерживаемых браузерах, если это неприемлемо. Подробнее см. в документации scheduler.yield()
в разделе Оптимизация длинных задач .
Типы wicg-task-scheduling
также можно использовать для проверки типов и поддержки IDE, если вы обнаруживаете функции scheduler.yield()
и добавляете резервный вариант самостоятельно.
Узнать больше
Дополнительную информацию об API и его взаимодействии с приоритетами задач и scheduler.postTask()
можно найти в документации scheduler.yield()
и Prioritized Task Scheduling на MDN.
Чтобы узнать больше о длительных задачах, о том, как они влияют на пользовательский опыт и что с ними делать, прочтите статью об оптимизации длительных задач .