Общедоступные советы по ускорению работы ваших приложений JavaScript часто включают в себя «Не блокируйте основной поток» и «Разбивайте свои длинные задачи». На этой странице объясняется, что означает этот совет и почему важна оптимизация задач в JavaScript.
Что такое задача?
Задача — это любая отдельная часть работы, которую выполняет браузер. Сюда входит рендеринг, синтаксический анализ HTML и CSS, выполнение написанного вами кода JavaScript и другие вещи, над которыми вы, возможно, не имеете прямого контроля. JavaScript ваших страниц является основным источником задач браузера.
Задачи влияют на производительность несколькими способами. Например, когда браузер загружает файл JavaScript во время запуска, он ставит в очередь задачи для анализа и компиляции этого JavaScript, чтобы его можно было выполнить. Позже в жизненном цикле страницы, когда ваш JavaScript работает, начинаются другие задачи, такие как управление взаимодействием через обработчики событий, анимацию на основе JavaScript и фоновые действия, такие как сбор аналитики. Все это, за исключением веб-воркеров и подобных API, происходит в основном потоке.
Какова основная нить?
Основной поток — это место, где в браузере выполняется большинство задач и где выполняется почти весь написанный вами код JavaScript.
Основной поток может обрабатывать только одну задачу одновременно. Любая задача, которая занимает более 50 миллисекунд, считается длинной задачей . Если пользователь пытается взаимодействовать со страницей во время длительной задачи или обновления рендеринга, браузеру приходится ждать, чтобы обработать это взаимодействие, что приводит к задержке.
Чтобы предотвратить это, разделите каждую длинную задачу на более мелкие задачи, каждая из которых требует меньше времени для выполнения. Это называется разбиением длинных задач.
Разбиение задач дает браузеру больше возможностей реагировать на более приоритетную работу, включая взаимодействие с пользователем, между другими задачами. Это позволяет осуществлять взаимодействие намного быстрее, тогда как в противном случае пользователь мог бы заметить задержку, пока браузер ждал завершения долгой задачи.
Стратегии управления задачами
JavaScript рассматривает каждую функцию как отдельную задачу, поскольку использует модель выполнения задачи до завершения . Это означает, что функция, вызывающая несколько других функций, как в следующем примере, должна выполняться до тех пор, пока не завершатся все вызванные функции, что замедляет работу браузера:
function saveSettings () { //This is a long task.
validateForm();
showSpinner();
saveToDatabase();
updateUI();
sendAnalytics();
}
Если ваш код содержит функции, вызывающие несколько методов, разделите его на несколько функций. Это не только дает браузеру больше возможностей реагировать на взаимодействие, но также упрощает чтение, поддержку и написание тестов вашего кода. В следующих разделах рассматриваются некоторые стратегии разделения длинных функций и определения приоритетности задач, из которых они состоят.
Вручную отложить выполнение кода
Вы можете отложить выполнение некоторых задач, передав соответствующую функцию в 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();
}
}
Ключевой момент: вам не обязательно уступать после каждого вызова функции. Например, если вы запускаете две функции, которые приводят к критическим обновлениям пользовательского интерфейса, вы, вероятно, не захотите прерывать их. Если можете, пусть сначала выполняется эта работа, а затем рассмотрите возможность перехода между функциями, которые выполняют фоновую или менее важную работу, которую пользователь не видит.
Специальный API планировщика
Упомянутые до сих пор API-интерфейсы могут помочь вам разбить задачи, но у них есть существенный недостаток: когда вы уступаете основному потоку, откладывая выполнение кода в более поздней задаче, этот код добавляется в конец очереди задач.
Если вы контролируете весь код на своей странице, вы можете создать собственный планировщик для определения приоритетов задач. Однако сторонние скрипты не будут использовать ваш планировщик, поэтому в этом случае вы не сможете расставить приоритеты в работе. Вы можете только разбить его или подчиниться взаимодействию с пользователем.
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'});
};
Здесь приоритет задач планируется таким образом, чтобы задачи с приоритетом браузера, такие как взаимодействие с пользователем, могли выполняться.
Вы также можете создавать экземпляры различных объектов 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.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 , любезно предоставлено Амирали Мирхашемяном .