Back

Фоновые задачи в браузере с помощью Scheduler API

Фоновые задачи в браузере с помощью Scheduler API

Ваше приложение кажется быстрым — до тех пор, пока это не так. Пользователь нажимает кнопку, и ничего не происходит в течение 300 миллисекунд, потому что главный поток занят обработкой того, что вовсе не требовалось делать прямо сейчас. Именно эту фундаментальную проблему призван решить Prioritized Task Scheduling API — обычно называемый Scheduler API.

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

Ключевые выводы

  • Scheduler API предоставляет точный контроль над тем, когда и как выполняется работа в главном потоке, с тремя уровнями приоритета: user-blocking, user-visible и background.
  • scheduler.postTask() откладывает работу с явным приоритетом, а scheduler.yield() разбивает длинные задачи, чтобы браузер мог обрабатывать пользовательский ввод между шагами.
  • Ни один из этих методов не переносит работу за пределы главного потока. Для настоящего параллелизма используйте Web Workers.
  • Браузеры на базе Chromium и Firefox поддерживают этот API; Safari — нет, поэтому всегда добавляйте резервный вариант с setTimeout.
  • Более грамотное планирование может улучшить Interaction to Next Paint (INP) и общую отзывчивость.

Почему важно планирование главного потока

JavaScript выполняется в одном потоке. Каждое исполнение скрипта, обновление DOM и обработчик событий конкурируют за один и тот же ресурс. Когда длинная задача блокирует этот поток, браузер не может реагировать на пользовательский ввод — и это напрямую снижает ваш показатель Interaction to Next Paint (INP).

Традиционные обходные пути имеют свои ограничения:

  • setTimeout(fn, 0) откладывает работу, но не даёт контроля над приоритетом. Она выполняется независимо от того, насколько загружен поток.
  • requestIdleCallback() ждёт простоя, что полезно для низкоприоритетной работы, но не имеет системы приоритетов, ограниченно поддерживается в Safari и может откладываться неопределённо долго под нагрузкой.

Scheduler API решает обе эти проблемы.

Как работает scheduler.postTask()

scheduler.postTask() — основная точка входа в Prioritized Task Scheduling API. Она планирует выполнение колбэка в главном потоке с указанным уровнем приоритета.

scheduler.postTask(() => {
  sendAnalyticsEvent('page_view');
}, { priority: 'background' });

Существует три уровня приоритета:

  • user-blocking — наивысший приоритет, для работы, которая напрямую блокирует взаимодействие с пользователем
  • user-visible — значение по умолчанию, для работы, которая влияет на то, что видит пользователь, но не блокирует его
  • background — самый низкий приоритет, для аналитики, предзагрузки или очистки

В этом ключевое отличие от старых API: вы не просто откладываете работу — вы сообщаете браузеру, насколько она важна по сравнению со всем остальным в очереди.

scheduler.postTask() возвращает Promise, что упрощает его использование с async/await и корректную обработку ошибок:

try {
  await scheduler.postTask(() => processLargeDataset(data), {
    priority: 'background'
  });
} catch (err) {
  // Task was aborted or failed
  console.warn('Task did not complete:', err);
}

Разбиение длинных задач с помощью scheduler.yield()

scheduler.yield() — более новое дополнение, решающее иную задачу: что делать, когда вы уже находитесь внутри длинной задачи и нужно дать браузеру возможность обработать ввод, прежде чем продолжить?

async function processItems(items) {
  for (const item of items) {
    processItem(item);
    await scheduler.yield(); // yield back to the browser between items
  }
}

Каждый await scheduler.yield() создаёт контрольную точку, в которой браузер может обработать ожидающие пользовательские взаимодействия перед возобновлением вашего цикла. По умолчанию продолжение после yield() планируется с приоритетом user-visible, хотя оно может унаследовать приоритет от окружающего вызова postTask(). Это один из самых практичных инструментов для сокращения длинных задач без необходимости перестраивать весь код.

Важное уточнение

Ни scheduler.postTask(), ни scheduler.yield() не переносят работу за пределы главного потока. Задачи, запланированные таким образом, по-прежнему выполняются в главном потоке — они просто более разумно ставятся в очередь и приоритизируются. Если вам нужно настоящее параллельное выполнение, для этого существуют Web Workers.

Поддержка в браузерах

Поддержка Scheduler API уверенно реализована в браузерах на базе Chromium (Chrome, Edge, Opera). Поддержка в Firefox появилась значительно позже, чем в Chromium, а Safari на данный момент по-прежнему не поддерживает этот API. Актуальную матрицу поддержки можно проверить на Can I Use.

Минимальная проверка поддержки перед использованием:

if ('scheduler' in window && 'postTask' in scheduler) {
  scheduler.postTask(myTask, { priority: 'background' });
} else {
  // Fallback for Safari and older browsers
  setTimeout(myTask, 0);
}

Что касается конкретно scheduler.yield(), проверяйте его поддержку отдельно, прежде чем полагаться на него в продакшене, поскольку он появился позже, чем postTask(), и имеет более узкое покрытие:

async function safeYield() {
  if ('scheduler' in window && 'yield' in scheduler) {
    await scheduler.yield();
  } else {
    await new Promise(resolve => setTimeout(resolve, 0));
  }
}

Когда обращаться к Scheduler API

Используйте scheduler.postTask(), когда вам нужно отложить некритичную работу — аналитику, предзагрузку или обработку после рендеринга — с явным контролем приоритета. Используйте scheduler.yield(), когда у вас есть цикл или многошаговый процесс, который рискует заблокировать обработку ввода.

Если ваши пользователи в основном используют Chromium или Firefox, Scheduler API уже сегодня можно практически применять — при условии наличия проверки поддержки и резервных вариантов. Для более широкого охвата сохраняйте резервный вариант с setTimeout, пока Safari не подтянется.

Заключение

Scheduler API закрывает давний пробел в том, как веб-платформа обрабатывает работу в главном потоке. Вместо использования грубых инструментов вроде setTimeout(fn, 0) или непоследовательно поддерживаемого requestIdleCallback(), теперь вы можете выражать намерение: эта задача критична, та может подождать, а этот цикл должен сделать паузу для пользовательского ввода. Результатом могут стать более плавные взаимодействия и улучшенные показатели INP при относительно небольших изменениях в коде. В сочетании с проверкой поддержки и разумным резервным вариантом вы можете безопасно внедрить его уже сегодня.

Часто задаваемые вопросы

Нет. Все задачи, запланированные с помощью scheduler.postTask(), по-прежнему выполняются в главном потоке. API меняет только то, когда и в каком порядке выполняются задачи, присваивая им приоритеты. Если вам нужен настоящий параллелизм для ресурсоёмких вычислений, используйте Web Workers — они выполняют код в отдельном потоке и обмениваются данными с главным потоком через сообщения.

Используйте scheduler.yield() внутри существующей функции или цикла, когда хотите кратко приостановиться, чтобы браузер мог обработать пользовательский ввод, а затем продолжить. Используйте scheduler.postTask(), когда нужно запланировать дискретную единицу работы для последующего выполнения с определённым приоритетом. Они решают связанные, но разные задачи: уступка управления в середине задачи против постановки новых задач в очередь.

INP измеряет, насколько быстро ваша страница реагирует на пользовательские взаимодействия. Длинные задачи в главном потоке — самая распространённая причина плохого INP. Уступая управление между шагами и назначая более низкий приоритет некритичной работе, Scheduler API помогает поддерживать главный поток доступным для оперативной обработки кликов, тапов и нажатий клавиш, что может снизить задержку взаимодействия.

Да, при условии что вы добавляете проверку поддержки и резервный вариант. Простой шаблон — проверить наличие scheduler.postTask и использовать setTimeout, когда он недоступен. Это сохраняет работоспособность вашего кода в Safari и старых браузерах, позволяя пользователям Chromium и Firefox получать преимущества приоритизированного планирования. Полифиллы существуют, но для базового использования они обычно не нужны.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay