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

Общедоступные советы по ускорению работы ваших приложений JavaScript часто включают в себя «Не блокируйте основной поток» и «Разбивайте свои длинные задачи». На этой странице объясняется, что означает этот совет и почему важна оптимизация задач в JavaScript.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

function saveSettings () { //This is a long task.
  validateForm();
  showSpinner();
  saveToDatabase();
  updateUI();
  sendAnalytics();
}
Функция saveSettings отображается в профилировщике производительности Chrome. Хотя функция верхнего уровня вызывает пять других функций, вся работа выполняется в одной длинной задаче, блокирующей основной поток.
Одна функция saveSettings() , вызывающая пять функций. Работа выполняется как часть одной длинной монолитной задачи.

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

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

Вы можете отложить выполнение некоторых задач, передав соответствующую функцию в 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);
}

Лучше всего это работает для ряда функций, которые необходимо выполнять по порядку. Код, организованный по-другому, требует другого подхода. Следующий пример — функция, которая обрабатывает большой объем данных с помощью цикла. Чем больше набор данных, тем больше времени это занимает, и не обязательно в цикле есть подходящее место для размещения setTimeout() :

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

К счастью, есть несколько других API, которые позволяют отложить выполнение кода на более позднюю задачу. Мы рекомендуем использовать postMessage() для более быстрого тайм-аута .

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

Используйте async / await для создания точек доходности

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

Самый простой способ сделать это — использовать Promise , который разрешается вызовом setTimeout() :

function yieldToMain () {
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  });
}

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

async function saveSettings () {
  // Create an array of functions to run:
  const tasks = [
    validateForm,
    showSpinner,
    saveToDatabase,
    updateUI,
    sendAnalytics
  ]

  // Loop over the tasks:
  while (tasks.length > 0) {
    // Shift the first task off the tasks array:
    const task = tasks.shift();

    // Run the task:
    task();

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

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

Та же функция saveSettings в профилировщике производительности Chrome, теперь с выходом. Задача теперь разбита на пять отдельных задач, по одной для каждой функции.
Функция saveSettings() теперь выполняет свои дочерние функции как отдельные задачи.

Специальный API планировщика

Упомянутые до сих пор API-интерфейсы могут помочь вам разбить задачи, но у них есть существенный недостаток: когда вы уступаете основному потоку, откладывая выполнение кода в более поздней задаче, этот код добавляется в конец очереди задач.

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

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

  • 94
  • 94
  • Икс

Источник

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

API postTask() имеет три доступных приоритета:

  • 'background' для задач с самым низким приоритетом.
  • 'user-visible' для задач со средним приоритетом. Это значение по умолчанию, если priority не установлен.
  • 'user-blocking' для критически важных задач, которые необходимо выполнять с высоким приоритетом.

В следующем примере кода API postTask() используется для запуска трех задач с максимально возможным приоритетом, а оставшихся двух задач — с минимально возможным приоритетом:

function saveSettings () {
  // Validate the form at high priority
  scheduler.postTask(validateForm, {priority: 'user-blocking'});

  // Show the spinner at high priority:
  scheduler.postTask(showSpinner, {priority: 'user-blocking'});

  // Update the database in the background:
  scheduler.postTask(saveToDatabase, {priority: 'background'});

  // Update the user interface at high priority:
  scheduler.postTask(updateUI, {priority: 'user-blocking'});

  // Send analytics data in the background:
  scheduler.postTask(sendAnalytics, {priority: 'background'});
};

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

Функция saveSettings, показанная в профилировщике производительности Chrome, но с использованием postTask. postTask разделяет каждую функцию, выполняемую saveSettings, и определяет их приоритет, чтобы взаимодействие с пользователем могло выполняться без блокировки.
При запуске saveSettings() функция планирует отдельные вызовы функций с помощью postTask() . Критическая работа, связанная с пользователем, запланирована с высоким приоритетом, а работа, о которой пользователь не знает, запланирована для выполнения в фоновом режиме. Это позволяет ускорить взаимодействие с пользователем, поскольку работа разбита на части и соответствующим образом расставлена ​​по приоритетам.

Вы также можете создавать экземпляры различных объектов TaskController , которые разделяют приоритеты между задачами, включая возможность изменять приоритеты для разных экземпляров TaskController по мере необходимости.

Встроенный выход с продолжением с использованием будущего API scheduler.yield()

Ключевой момент: для более подробного объяснения scheduler.yield() прочитайте о его пробной версии (после ее завершения), а также о его объяснении .

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

async function saveSettings () {
  // Create an array of functions to run:
  const tasks = [
    validateForm,
    showSpinner,
    saveToDatabase,
    updateUI,
    sendAnalytics
  ]

  // Loop over the tasks:
  while (tasks.length > 0) {
    // Shift the first task off the tasks array:
    const task = tasks.shift();

    // Run the task:
    task();

    // Yield to the main thread with the scheduler
    // API's own yielding mechanism:
    await scheduler.yield();
  }
}

Этот код во многом знаком, но вместо использования yieldToMain() он использует await scheduler.yield() .

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

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

Использование scheduler.postTask() с priority: 'user-blocking' также имеет высокую вероятность продолжения из-за высокого приоритета user-blocking , поэтому вы можете использовать его в качестве альтернативы, пока scheduler.yield() не станет более широко доступным.

Использование setTimeout() (или scheduler.postTask() с priority: 'user-visible' или без явного priority ) планирует задачу в конце очереди, позволяя другим ожидающим задачам выполняться до продолжения.

Доходность при вводе с помощью isInputPending()

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

  • 87
  • 87
  • Икс
  • Икс

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

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

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

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

Заключение

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

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

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

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

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