Оптимизируйте длинные задачи

Вам говорили «не блокируйте основной поток» и «разбивайте свои длинные задачи», но что значит делать эти вещи?

Опубликовано: 30 сентября 2022 г. Последнее обновление: 19 декабря 2024 г.

Общие советы по обеспечению быстроты JavaScript-приложений сводятся к следующим советам:

  • «Не блокируйте основной поток».
  • «Разбивайте свои длинные задачи».

Это отличный совет, но какую работу он включает? Меньшее количество JavaScript — это хорошо, но означает ли это автоматически более отзывчивый пользовательский интерфейс? Может быть, а может и нет.

Чтобы понять, как оптимизировать задачи в JavaScript, сначала нужно знать, что это за задачи и как с ними справляется браузер.

Что такое задача?

Задача — это любая отдельная часть работы, которую выполняет браузер. Эта работа включает в себя рендеринг, анализ HTML и CSS, запуск JavaScript и другие виды работы, которые вы не можете контролировать напрямую. Из всего этого написанный вами JavaScript является, пожалуй, крупнейшим источником задач.

Визуализация задачи, как показано в профиле производительности DevTools Chrome. Задача находится на вершине стека, а под ней находится обработчик событий щелчка, вызов функции и другие элементы. Задача также включает в себя некоторые работы по рендерингу с правой стороны.
Задача, запускаемая обработчиком событий click , показанная в профилировщике производительности Chrome DevTools.

Задачи, связанные с JavaScript, влияют на производительность несколькими способами:

  • Когда браузер загружает файл JavaScript во время запуска, он ставит в очередь задачи для анализа и компиляции этого JavaScript, чтобы его можно было выполнить позже.
  • В других случаях в течение жизни страницы задачи ставятся в очередь, когда JavaScript работает, например, реагирование на взаимодействия через обработчики событий, анимацию на основе JavaScript и фоновые действия, такие как сбор аналитики.

Все это — за исключением веб-воркеров и подобных API — происходит в основном потоке.

Какова основная нить?

Основной поток — это место, где в браузере выполняется большинство задач и где выполняется почти весь написанный вами код JavaScript.

Основной поток может обрабатывать только одну задачу одновременно. Любая задача, которая занимает более 50 миллисекунд, является долгой задачей . Для задач, длительность которых превышает 50 миллисекунд, общее время задачи минус 50 миллисекунд называется периодом блокировки задачи.

Браузер блокирует взаимодействие во время выполнения задачи любой длины, но это незаметно для пользователя, пока задачи не выполняются слишком долго. Однако когда пользователь пытается взаимодействовать со страницей, когда имеется много длительных задач, пользовательский интерфейс будет не отвечать на запросы и, возможно, даже сломается, если основной поток заблокирован на очень длительные периоды времени.

Длинная задача в профилировщике производительности DevTools Chrome. Блокирующая часть задачи (более 50 миллисекунд) обозначена узором из красных диагональных полос.
Длинная задача, как показано в профилировщике производительности Chrome. Длинные задачи обозначаются красным треугольником в углу задачи, а блокирующая часть задачи заполнена узором из диагональных красных полос.

Чтобы основной поток не блокировался слишком надолго, можно разбить длинную задачу на несколько более мелких.

Отдельная длинная задача по сравнению с той же задачей, разбитой на более короткие задачи. Длинная задача представляет собой один большой прямоугольник, тогда как разбитая на части задача состоит из пяти меньших блоков, которые в совокупности имеют ту же ширину, что и длинная задача.
Визуализация одной длинной задачи в сравнении с той же задачей, разбитой на пять более коротких задач.

Это важно, потому что когда задачи разбиты на части, браузер может гораздо быстрее реагировать на более приоритетную работу, включая взаимодействие с пользователем. После этого оставшиеся задачи выполняются до завершения, гарантируя, что работа, которую вы изначально поставили в очередь, будет выполнена.

Изображение того, как разделение задачи может облегчить взаимодействие с пользователем. Вверху длинная задача блокирует запуск обработчика событий до завершения задачи. Внизу разбитая на части задача позволяет обработчику событий запуститься раньше, чем в противном случае.
Визуализация того, что происходит с взаимодействиями, когда задачи слишком длинные и браузер не может реагировать на взаимодействия достаточно быстро, а также когда более длинные задачи разбиваются на более мелкие задачи.

В верхней части предыдущего рисунка обработчик событий, поставленный в очередь в результате взаимодействия с пользователем, должен был дождаться одной длинной задачи, прежде чем она сможет начаться. Это задерживает взаимодействие. В этом сценарии пользователь мог заметить задержку. Внизу обработчик событий может начать выполняться раньше, и взаимодействие может ощущаться мгновенно .

Теперь, когда вы знаете, почему важно разбивать задачи, вы можете узнать, как это сделать с помощью JavaScript.

Стратегии управления задачами

Распространенный совет в архитектуре программного обеспечения — разбить вашу работу на более мелкие функции:

function saveSettings () {
  validateForm();
  showSpinner();
  saveToDatabase();
  updateUI();
  sendAnalytics();
}

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

Концептуально saveSettings() имеет хорошую архитектуру. Если вам нужно отладить одну из этих функций, вы можете просмотреть дерево проекта, чтобы выяснить, что делает каждая функция. Такое разделение работы упрощает навигацию и поддержку проектов.

Однако потенциальная проблема здесь заключается в том, что JavaScript не запускает каждую из этих функций как отдельные задачи, поскольку они выполняются внутри функции saveSettings() . Это означает, что все пять функций будут выполняться как одна задача.

Функция saveSettings, как показано в профилировщике производительности Chrome. Хотя функция верхнего уровня вызывает пять других функций, вся работа выполняется в одной длинной задаче, поэтому видимый пользователю результат запуска функции не виден до тех пор, пока все они не будут завершены.
Одна функция saveSettings() вызывающая пять функций. Работа выполняется как часть одной длинной монолитной задачи, блокирующей любую визуальную реакцию до тех пор, пока не будут выполнены все пять функций.

В лучшем случае даже одна из этих функций может увеличить общую продолжительность задачи на 50 или более миллисекунд. В худшем случае большинство из этих задач могут выполняться гораздо дольше, особенно на устройствах с ограниченными ресурсами.

В этом случае saveSettings() запускается щелчком пользователя, а поскольку браузер не может отобразить ответ до тех пор, пока вся функция не завершится, результатом этой длительной задачи будет медленный и не отвечающий на запросы пользовательский интерфейс, который будет измеряется как плохое взаимодействие со следующей отрисовкой (INP) .

Вручную отложить выполнение кода

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

Один из методов, который разработчики использовали для разбиения задач на более мелкие, — это setTimeout() . Используя этот метод, вы передаете функцию в setTimeout() . Это переносит выполнение обратного вызова в отдельную задачу, даже если вы укажете таймаут 0 .

function saveSettings () {
  // Do critical work that is user-visible:
  validateForm();
  showSpinner();
  updateUI();

  // Defer work that isn't user-visible to a separate task:
  setTimeout(() => {
    saveToDatabase();
    sendAnalytics();
  }, 0);
}

Это называется выходом и лучше всего работает для ряда функций, которые необходимо выполнять последовательно.

Однако ваш код не всегда может быть организован таким образом. Например, у вас может быть большой объем данных, которые необходимо обработать в цикле, и эта задача может занять очень много времени, если имеется много итераций.

function processData () {
  for (const item of largeDataArray) {
    // Process the individual item here.
  }
}

Использование setTimeout() здесь проблематично из-за эргономики разработчика, и после пяти раундов вложенных setTimeout() браузер начнет устанавливать задержку минимум в 5 миллисекунд для каждого дополнительного setTimeout() .

У setTimeout также есть еще один недостаток, когда дело доходит до передачи: когда вы уступаете основному потоку, откладывая выполнение кода в последующей задаче с помощью setTimeout , эта задача добавляется в конец очереди. Если есть другие задачи, ожидающие выполнения, они будут выполняться раньше вашего отложенного кода.

Специальный API доходности: scheduler.yield()

Browser Support

  • Хром: 129.
  • Край: 129.
  • Firefox: не поддерживается.
  • Сафари: не поддерживается.

Source

scheduler.yield() — это API, специально разработанный для передачи основного потока в браузере.

Это не синтаксис уровня языка или специальная конструкция; scheduler.yield() — это просто функция, возвращающая Promise , которое будет выполнено в будущей задаче. Любой код, предназначенный для запуска после разрешения этого Promise (либо в явной цепочке .then() , либо после его await в асинхронной функции), затем будет выполняться в этой будущей задаче.

На практике: вставьте await scheduler.yield() , и в этот момент функция приостановит выполнение и перейдет к основному потоку. Выполнение оставшейся части функции, называемой продолжением функции, будет запланировано в новой задаче цикла событий. Когда эта задача запустится, ожидаемое обещание будет выполнено, и функция продолжит выполнение с того места, где она остановилась.

async function saveSettings () {
  // Do critical work that is user-visible:
  validateForm();
  showSpinner();
  updateUI();

  // Yield to the main thread:
  await scheduler.yield()

  // Work that isn't user-visible, continued in a separate task:
  saveToDatabase();
  sendAnalytics();
}
Функция saveSettings, показанная в профилировщике производительности Chrome, теперь разбита на две задачи. Первая задача вызывает две функции, а затем завершает работу, позволяя выполнить работу по макету и рисованию и дать пользователю видимый ответ. В результате событие щелчка завершается гораздо быстрее — за 64 миллисекунды. Вторая задача вызывает последние три функции.
Выполнение функции saveSettings() теперь разделено на две задачи. В результате макетирование и рисование могут выполняться между задачами, что дает пользователю более быструю визуальную реакцию, что измеряется теперь гораздо более коротким взаимодействием с указателем.

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

Это позволяет избежать прерывания порядка выполнения вашего кода кодом из других источников задач, например задачами из сторонних скриптов.

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

Кроссбраузерная поддержка

scheduler.yield() пока не поддерживается во всех браузерах, поэтому необходим запасной вариант.

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

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

function yieldToMain () {
  if (globalThis.scheduler?.yield) {
    return scheduler.yield();
  }

  // Fall back to yielding with setTimeout.
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  });
}

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

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

Это можно сделать как путем обнаружения функций, так и путем возврата к ожиданию одной микрозадачи в удобной однострочной строке:

// Yield to the main thread if scheduler.yield() is available.
await globalThis.scheduler?.yield?.();

Прервите длительную работу с помощью scheduler.yield()

Преимущество использования любого из этих методов использования scheduler.yield() заключается в том, что вы можете await его в любой async функции.

Например, если вам нужно выполнить массив заданий, которые часто в конечном итоге образуют длинную задачу, вы можете вставить выходные данные, чтобы разбить задачу.

async function runJobs(jobQueue) {
  for (const job of jobQueue) {
    // Run the job:
    job();

    // Yield to the main thread:
    await yieldToMain();
  }
}

Продолжение runJobs() будет иметь приоритет, но по-прежнему позволит выполнять работу с более высоким приоритетом, например визуальное реагирование на ввод пользователя, без необходимости ждать завершения потенциально длинного списка заданий.

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

Один из подходов — группировать задания, передавая результаты между ними только в том случае, если с момента последнего выполнения прошло достаточно времени. Общий срок составляет 50 миллисекунд, чтобы задачи не превращались в длинные задачи, но его можно настроить как компромисс между скоростью реагирования и временем завершения очереди заданий.

async function runJobs(jobQueue, deadline=50) {
  let lastYield = performance.now();

  for (const job of jobQueue) {
    // Run the job:
    job();

    // If it's been longer than the deadline, yield to the main thread:
    if (performance.now() - lastYield > deadline) {
      await yieldToMain();
      lastYield = performance.now();
    }
  }
}

В результате задания разбиваются, чтобы выполнение никогда не занимало слишком много времени, но исполнитель уступает основному потоку примерно каждые 50 миллисекунд.

Серия рабочих функций, показанных на панели производительности Chrome DevTools, выполнение которых разбито на несколько задач.
Рабочие места объединены в несколько задач.

Не используйте isInputPending()

Browser Support

  • Хром: 87.
  • Край: 87.
  • Firefox: не поддерживается.
  • Сафари: не поддерживается.

Source

API isInputPending() предоставляет способ проверки того, пытался ли пользователь взаимодействовать со страницей, и уступает только в том случае, если ввод ожидает ответа.

Это позволяет JavaScript продолжать работу, если нет ожидающих входных данных, вместо того, чтобы уступить и оказаться в конце очереди задач. Это может привести к впечатляющему повышению производительности, как подробно описано в Intent to Ship , для сайтов, которые в противном случае не могли бы вернуться в основной поток.

Однако с момента запуска этого API наше понимание доходности возросло, особенно с появлением INP. Мы больше не рекомендуем использовать этот API , а вместо этого рекомендуем передавать данные независимо от того, ожидается ли ввод или нет, по ряду причин:

  • isInputPending() может ошибочно возвращать false несмотря на то, что пользователь взаимодействовал в некоторых обстоятельствах.
  • Ввод — не единственный случай, когда задачи должны давать результаты. Анимация и другие регулярные обновления пользовательского интерфейса могут быть не менее важны для создания адаптивной веб-страницы.
  • С тех пор были представлены более комплексные API-интерфейсы доходности, которые решают проблемы доходности, такие как scheduler.postTask() и scheduler.yield() .

Заключение

Управление задачами является сложной задачей, но это гарантирует, что ваша страница будет быстрее реагировать на взаимодействия с пользователем. Не существует единого совета по управлению задачами и расстановке приоритетов, а есть несколько различных методов. Еще раз повторю: вот основные моменты, которые следует учитывать при управлении задачами:

  • Перейдите в основной поток для решения критических задач, с которыми сталкивается пользователь.
  • Используйте scheduler.yield() (с кросс-браузерным резервным вариантом) для эргономичного выхода и получения продолжений с приоритетом.
  • Наконец, выполняйте как можно меньше работы в своих функциях.

Дополнительные сведения о scheduler.yield() , его явном родственнике scheduler.postTask() для планирования задач и приоритезации задач см. в документации по API планирования приоритетных задач .

С помощью одного или нескольких из этих инструментов вы сможете структурировать работу своего приложения так, чтобы оно отдавало приоритет потребностям пользователя, гарантируя при этом выполнение менее важной работы. Это улучшит пользовательский опыт, сделает его более отзывчивым и приятным в использовании.

Особая благодарность Филипу Уолтону за техническую проверку этого руководства.

Миниатюрное изображение взято с сайта Unsplash , любезно предоставлено Амирали Мирхашемяном .

,

Вам говорили «не блокируйте основной поток» и «разбивайте свои длинные задачи», но что значит делать эти вещи?

Опубликовано: 30 сентября 2022 г. Последнее обновление: 19 декабря 2024 г.

Общие советы по обеспечению быстроты JavaScript-приложений сводятся к следующим советам:

  • «Не блокируйте основной поток».
  • «Разбивайте свои длинные задачи».

Это отличный совет, но какую работу он включает? Меньшее количество JavaScript — это хорошо, но означает ли это автоматически более отзывчивый пользовательский интерфейс? Может быть, а может и нет.

Чтобы понять, как оптимизировать задачи в JavaScript, сначала нужно знать, что это за задачи и как с ними справляется браузер.

Что такое задача?

Задача — это любая отдельная часть работы, которую выполняет браузер. Эта работа включает в себя рендеринг, анализ HTML и CSS, запуск JavaScript и другие виды работы, которые вы не можете контролировать напрямую. Из всего этого написанный вами JavaScript является, пожалуй, крупнейшим источником задач.

Визуализация задачи, как показано в профиле производительности DevTools Chrome. Задача находится на вершине стека, а под ней находится обработчик событий щелчка, вызов функции и другие элементы. Задача также включает в себя некоторые работы по рендерингу с правой стороны.
Задача, запускаемая обработчиком событий click , показанная в профилировщике производительности Chrome DevTools.

Задачи, связанные с JavaScript, влияют на производительность несколькими способами:

  • Когда браузер загружает файл JavaScript во время запуска, он ставит в очередь задачи для анализа и компиляции этого JavaScript, чтобы его можно было выполнить позже.
  • В других случаях в течение жизни страницы задачи ставятся в очередь, когда JavaScript работает, например, реагирование на взаимодействия через обработчики событий, анимацию на основе JavaScript и фоновые действия, такие как сбор аналитики.

Все это — за исключением веб-воркеров и подобных API — происходит в основном потоке.

Какова основная нить?

Основной поток — это место, где в браузере выполняется большинство задач и где выполняется почти весь написанный вами код JavaScript.

Основной поток может обрабатывать только одну задачу одновременно. Любая задача, которая занимает более 50 миллисекунд, является долгой задачей . Для задач, длительность которых превышает 50 миллисекунд, общее время задачи минус 50 миллисекунд называется периодом блокировки задачи.

Браузер блокирует взаимодействие во время выполнения задачи любой длины, но это незаметно для пользователя, пока задачи не выполняются слишком долго. Однако когда пользователь пытается взаимодействовать со страницей, когда имеется много длительных задач, пользовательский интерфейс будет не отвечать на запросы и, возможно, даже сломается, если основной поток заблокирован на очень длительные периоды времени.

Длинная задача в профилировщике производительности DevTools Chrome. Блокирующая часть задачи (более 50 миллисекунд) обозначена узором из красных диагональных полос.
Длинная задача, как показано в профилировщике производительности Chrome. Длинные задачи обозначаются красным треугольником в углу задачи, а блокирующая часть задачи заполнена узором из диагональных красных полос.

Чтобы основной поток не блокировался слишком надолго, можно разбить длинную задачу на несколько более мелких.

Отдельная длинная задача по сравнению с той же задачей, разбитой на более короткие задачи. Длинная задача представляет собой один большой прямоугольник, тогда как разбитая на части задача состоит из пяти меньших блоков, которые в совокупности имеют ту же ширину, что и длинная задача.
Визуализация одной длинной задачи в сравнении с той же задачей, разбитой на пять более коротких задач.

Это важно, потому что когда задачи разбиты на части, браузер может гораздо быстрее реагировать на более приоритетную работу, включая взаимодействие с пользователем. После этого оставшиеся задачи выполняются до завершения, гарантируя, что работа, которую вы изначально поставили в очередь, будет выполнена.

Изображение того, как разделение задачи может облегчить взаимодействие с пользователем. Вверху длинная задача блокирует запуск обработчика событий до завершения задачи. Внизу разбитая на части задача позволяет обработчику событий запуститься раньше, чем в противном случае.
Визуализация того, что происходит с взаимодействиями, когда задачи слишком длинные и браузер не может реагировать на взаимодействия достаточно быстро, а также когда более длинные задачи разбиваются на более мелкие задачи.

В верхней части предыдущего рисунка обработчик событий, поставленный в очередь в результате взаимодействия с пользователем, должен был дождаться одной длинной задачи, прежде чем она сможет начаться. Это задерживает взаимодействие. В этом сценарии пользователь мог заметить задержку. Внизу обработчик событий может начать выполняться раньше, и взаимодействие может ощущаться мгновенно .

Теперь, когда вы знаете, почему важно разбивать задачи, вы можете узнать, как это сделать с помощью JavaScript.

Стратегии управления задачами

Распространенный совет в архитектуре программного обеспечения — разбить вашу работу на более мелкие функции:

function saveSettings () {
  validateForm();
  showSpinner();
  saveToDatabase();
  updateUI();
  sendAnalytics();
}

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

Концептуально saveSettings() имеет хорошую архитектуру. Если вам нужно отладить одну из этих функций, вы можете просмотреть дерево проекта, чтобы выяснить, что делает каждая функция. Такое разделение работы упрощает навигацию и поддержку проектов.

Однако потенциальная проблема здесь заключается в том, что JavaScript не запускает каждую из этих функций как отдельные задачи, поскольку они выполняются внутри функции saveSettings() . Это означает, что все пять функций будут выполняться как одна задача.

Функция saveSettings, как показано в профилировщике производительности Chrome. Хотя функция верхнего уровня вызывает пять других функций, вся работа выполняется в одной длинной задаче, поэтому видимый пользователю результат запуска функции не виден до тех пор, пока все они не будут завершены.
Одна функция saveSettings() вызывающая пять функций. Работа выполняется как часть одной длинной монолитной задачи, блокирующей любую визуальную реакцию до тех пор, пока не будут выполнены все пять функций.

В лучшем случае даже одна из этих функций может увеличить общую продолжительность задачи на 50 или более миллисекунд. В худшем случае большинство из этих задач могут выполняться гораздо дольше, особенно на устройствах с ограниченными ресурсами.

В этом случае saveSettings() запускается щелчком пользователя, а поскольку браузер не может отобразить ответ до тех пор, пока вся функция не завершится, результатом этой длительной задачи будет медленный и не отвечающий на запросы пользовательский интерфейс, который будет измеряется как плохое взаимодействие со следующей отрисовкой (INP) .

Вручную отложить выполнение кода

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

Один из методов, который разработчики использовали для разбиения задач на более мелкие, — это setTimeout() . Используя этот метод, вы передаете функцию в setTimeout() . Это переносит выполнение обратного вызова в отдельную задачу, даже если вы укажете таймаут 0 .

function saveSettings () {
  // Do critical work that is user-visible:
  validateForm();
  showSpinner();
  updateUI();

  // Defer work that isn't user-visible to a separate task:
  setTimeout(() => {
    saveToDatabase();
    sendAnalytics();
  }, 0);
}

Это называется выходом и лучше всего работает для ряда функций, которые необходимо выполнять последовательно.

Однако ваш код не всегда может быть организован таким образом. Например, у вас может быть большой объем данных, которые необходимо обработать в цикле, и эта задача может занять очень много времени, если имеется много итераций.

function processData () {
  for (const item of largeDataArray) {
    // Process the individual item here.
  }
}

Использование setTimeout() здесь проблематично из-за эргономики разработчика, и после пяти раундов вложенных setTimeout() браузер начнет устанавливать задержку минимум в 5 миллисекунд для каждого дополнительного setTimeout() .

У setTimeout также есть еще один недостаток, когда дело доходит до передачи: когда вы уступаете основному потоку, откладывая выполнение кода в последующей задаче с помощью setTimeout , эта задача добавляется в конец очереди. Если есть другие задачи, ожидающие выполнения, они будут выполняться раньше вашего отложенного кода.

Специальный API доходности: scheduler.yield()

Browser Support

  • Хром: 129.
  • Край: 129.
  • Firefox: не поддерживается.
  • Сафари: не поддерживается.

Source

scheduler.yield() — это API, специально разработанный для передачи основного потока в браузере.

Это не синтаксис уровня языка или специальная конструкция; scheduler.yield() — это просто функция, возвращающая Promise , которое будет выполнено в будущей задаче. Любой код, предназначенный для запуска после разрешения этого Promise (либо в явной цепочке .then() , либо после его await в асинхронной функции), затем будет выполняться в этой будущей задаче.

На практике: вставьте await scheduler.yield() , и в этот момент функция приостановит выполнение и перейдет к основному потоку. Выполнение оставшейся части функции, называемой продолжением функции, будет запланировано в новой задаче цикла событий. Когда эта задача запустится, ожидаемое обещание будет выполнено, и функция продолжит выполнение с того места, где она остановилась.

async function saveSettings () {
  // Do critical work that is user-visible:
  validateForm();
  showSpinner();
  updateUI();

  // Yield to the main thread:
  await scheduler.yield()

  // Work that isn't user-visible, continued in a separate task:
  saveToDatabase();
  sendAnalytics();
}
Функция saveSettings, показанная в профилировщике производительности Chrome, теперь разбита на две задачи. Первая задача вызывает две функции, а затем завершает работу, позволяя выполнить работу по макету и рисованию и дать пользователю видимый ответ. В результате событие щелчка завершается гораздо быстрее — за 64 миллисекунды. Вторая задача вызывает последние три функции.
Выполнение функции saveSettings() теперь разделено на две задачи. В результате макетирование и рисование могут выполняться между задачами, что дает пользователю более быструю визуальную реакцию, что измеряется теперь гораздо более коротким взаимодействием с указателем.

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

Это позволяет избежать прерывания порядка выполнения вашего кода кодом из других источников задач, например задачами из сторонних скриптов.

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

Кроссбраузерная поддержка

scheduler.yield() пока не поддерживается во всех браузерах, поэтому необходим запасной вариант.

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

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

function yieldToMain () {
  if (globalThis.scheduler?.yield) {
    return scheduler.yield();
  }

  // Fall back to yielding with setTimeout.
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  });
}

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

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

Это можно сделать как путем обнаружения функций, так и путем возврата к ожиданию одной микрозадачи в удобной однострочной строке:

// Yield to the main thread if scheduler.yield() is available.
await globalThis.scheduler?.yield?.();

Прервите длительную работу с помощью scheduler.yield()

Преимущество использования любого из этих методов использования scheduler.yield() заключается в том, что вы можете await его в любой async функции.

Например, если вам нужно выполнить массив заданий, которые часто в конечном итоге образуют длинную задачу, вы можете вставить выходные данные, чтобы разбить задачу.

async function runJobs(jobQueue) {
  for (const job of jobQueue) {
    // Run the job:
    job();

    // Yield to the main thread:
    await yieldToMain();
  }
}

Продолжение runJobs() будет иметь приоритет, но по-прежнему позволит выполнять работу с более высоким приоритетом, например визуальное реагирование на ввод пользователя, без необходимости ждать завершения потенциально длинного списка заданий.

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

Один из подходов — группировать задания, передавая результаты между ними только в том случае, если с момента последнего выполнения прошло достаточно времени. Общий срок составляет 50 миллисекунд, чтобы задачи не превращались в длинные задачи, но его можно настроить как компромисс между скоростью реагирования и временем завершения очереди заданий.

async function runJobs(jobQueue, deadline=50) {
  let lastYield = performance.now();

  for (const job of jobQueue) {
    // Run the job:
    job();

    // If it's been longer than the deadline, yield to the main thread:
    if (performance.now() - lastYield > deadline) {
      await yieldToMain();
      lastYield = performance.now();
    }
  }
}

В результате задания разбиваются, чтобы выполнение никогда не занимало слишком много времени, но исполнитель уступает основному потоку примерно каждые 50 миллисекунд.

Серия рабочих функций, показанных на панели производительности Chrome DevTools, выполнение которых разбито на несколько задач.
Рабочие места объединены в несколько задач.

Не используйте isInputPending()

Browser Support

  • Хром: 87.
  • Край: 87.
  • Firefox: не поддерживается.
  • Сафари: не поддерживается.

Source

API isInputPending() предоставляет способ проверки того, пытался ли пользователь взаимодействовать со страницей, и уступает только в том случае, если ввод ожидает ответа.

Это позволяет JavaScript продолжать работу, если нет ожидающих входных данных, вместо того, чтобы уступить и оказаться в конце очереди задач. Это может привести к впечатляющему повышению производительности, как подробно описано в Intent to Ship , для сайтов, которые в противном случае не могли бы вернуться в основной поток.

Однако с момента запуска этого API наше понимание доходности возросло, особенно с появлением INP. Мы больше не рекомендуем использовать этот API , а вместо этого рекомендуем передавать данные независимо от того, ожидается ли ввод или нет, по ряду причин:

  • isInputPending() может ошибочно возвращать false несмотря на то, что пользователь взаимодействовал в некоторых обстоятельствах.
  • Ввод — не единственный случай, когда задачи должны давать результат. Анимации и другие обычные обновления пользовательского интерфейса могут быть одинаково важны для предоставления адаптивной веб -страницы.
  • С тех пор были введены более комплексные API -интерфейсы, которые обращаются к проблемам, таким как scheduler.postTask() и scheduler.yield() .

Заключение

Управление задачами сложно, но это гарантирует, что ваша страница быстрее реагирует на взаимодействие с пользователями. Там нет единого совета по управлению и приоритетным задачам, а скорее ряд различных методов. Чтобы повторить, это основные вещи, которые вы захотите рассмотреть при управлении задачами:

  • Получите основной поток для критических, пользовательских задач.
  • Используйте scheduler.yield()
  • Наконец, сделайте как можно меньше работы в своих функциях.

scheduler.postTask() узнать больше о scheduler.yield() .

С помощью одного или нескольких из этих инструментов вы должны иметь возможность структурировать работу в вашем приложении, чтобы она приоритет потребностям пользователя, обеспечивая при этом, чтобы менее критическая работа была все еще выполнена. Это создаст лучший пользовательский опыт, который будет более отзывчивым и более приятным в использовании.

Особая благодарность Филиппу Уолтону за его техническое проверок этого руководства.

Миниатюрный образ, полученный из Unsplash , любезно предоставленной Амирали Мирхашемян .