Оптимизация выполнения JavaScript

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

TL;DR

  • Не используйте функции setTimeout или setInterval для внесения визуальных изменений; вместо этого всегда пользуйтесь функцией requestAnimationFrame.
  • Перемещайте скрипты JavaScript, которые выполняются долго, за пределы основного потока в рабочие веб-процессы Web Worker.
  • Для внесения изменений в DOM-элементы за несколько кадров используйте микрозадачи.
  • Для оценки влияния JavaScript используйте шкалу времени и средство профилирования JavaScript из Chrome DevTools.

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

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

Используйте функцию requestAnimationFrame для внесения визуальных изменений

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

/**
 * If run as a requestAnimationFrame callback, this
 * will be run at the start of the frame.
 */
function updateScreen(time) {
  // Make visual updates here.
}

requestAnimationFrame(updateScreen);

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

Функция setTimeout, из-за которой браузер пропускает кадр.

Скажу больше, на сегодня для animate jQuery по умолчанию использует setTimeout! Можно установить исправление, чтобы использовалась функция requestAnimationFrame, что настоятельно рекомендуется сделать.

Снижайте сложность или используйте рабочие веб-процессы Web Worker

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

Следует тактически грамотно выбирать время и продолжительность выполнения JavaScript. Например, если выполняется такая анимация, как прокрутка, идеальным будет выполнение JavaScript в течение первых 3–4 мс. Чуть дольше – и вы рискуете занять слишком много времени. Если же в данный момент никаких действий не выполняется, то за временем работы можно позволить себе следить не так строго.

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

var dataSortWorker = new Worker("sort-worker.js");
dataSortWorker.postMesssage(dataToSort);

// The main thread is now free to continue working on other things...

dataSortWorker.addEventListener('message', function(evt) {
   var sortedData = e.data;
   // Update data on screen...
});

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

var taskList = breakBigTaskIntoMicroTasks(monsterTaskList);
requestAnimationFrame(processTaskList);

function processTaskList(taskStartTime) {
  var taskFinishTime;

  do {
    // Assume the next task is pushed onto a stack.
    var nextTask = taskList.pop();

    // Process nextTask.
    processTask(nextTask);

    // Go again if there’s enough time to do the next task.
    taskFinishTime = window.performance.now();
  } while (taskFinishTime - taskStartTime < 3);

  if (taskList.length > 0)
    requestAnimationFrame(processTaskList);

}

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

Знайте, как ваш код JavaScript влияет на кадры

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

Лучше всего определять затраты на выполнение кода JavaScript и его профиль производительности с помощью Chrome DevTools. Обычно программа выдает не очень подробные записи следующего вида:

Шкала времени Chrome DevTools с малоинформативными сведениями о выполнении JS.

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

Включение средства профилирования JS в DevTools.

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

Шкала времени Chrome DevTools с большим объемом информации о выполнении JS.

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

Избегайте микрооптимизации кода JavaScript

Возможно, это круто ― знать, что браузер может выполнить одну версию кода в 100 раз быстрее, чем другую, например, что запросы или offsetTop элемента быстрее, чем вычисление getBoundingClientRect(). Однако почти всегда верно, что такие функции вызываются лишь несколько раз за кадр, поэтому уделять этой стороне работы JavaScript основное внимание – это просто пустая трата времени. Сэкономить удастся лишь доли миллисекунды.

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

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