Опубликовано: 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)
.
Планирование приоритетных задач просто делает это более явным, облегчая определение того, какая задача будет выполнена раньше другой, и позволяет корректировать приоритеты, чтобы при необходимости изменить порядок выполнения.
Как уже упоминалось, продолжение выполнения функции после передачи управления с помощью scheduler.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.
Чтобы узнать больше о длительных задачах, о том, как они влияют на пользовательский опыт и что с ними делать, прочитайте статью об оптимизации длительных задач .