Улучшение производительности в JavaScript с помощью таймера

в 12:49, , рубрики: javascript, метки:

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

// Антишаблон, не использовать!
for (var i = 0; i < 1000000; i ++) {
    console.log(i);
}


В данном случае браузер просто перестанет реагировать и повиснет.

Основные правила при использовании циклов при больших итерациях:

  1. счет итераций должен быть в обратном порядке — от максимального значения до нуля. Это дает дополнительный прирост в скорости из-за того, что операция сравнения с числом 0 немного эффективнее, чем операция сравнения с длинной массива или любым другим числом, отличным от 0;
  2. если известна длина цикла, то ее необходимо сохранить в отдельную переменную, так как при каждой итерации обращаться к свойству length снижает производительность;
  3. если вы используете JSLint, то советую оператор ++ или — заменить на +=1 и -=1, соответственно;
  4. в javascript циклы for, while и do while абсолютно одинаковы по скорости производительности, что нельзя сказать о for in, поэтому не важно какой именно цикл вы используете и старайтесь избегать for in там, где это возможно.

// Антишаблон, не использовать!
var count = 1000000;

while (count -= 1) {
    console.log(count);
}

Даже если нам заранее известно количество итераций в цикле и если мы запустим цикл с последнего элемента и пойдем в порядке убывания, — как показано в примере выше — то браузер достигнув цикла, заставит пользователя ждать(в лучшем случае). И хорошо, если script расположен перед закрывающим тегом body, а не в head. В противном случае javascript не даст загрузиться html документу, пока полностью не будет выполнен.

Решение этой проблемы состоит в следующем:

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

var count = 1000000,
    parts = 500, // разбиваем на 500 частей, то есть в каждой части у нас будет 2000 итераций(1000000 / 500 = 2000)
    maxIterations = count / parts, // собственно итерации для каждой части
    iteration = 0; // счетчик итераций

setTimeout(function createCycle() {
    /*
     Для разнообразия привел цикл for, но как писал уже выше, разницы между while и for по производительности нет;
     так же цикл сделал по возрастанию, чтоб показать, что даже в таком случае производительность НАМНОГО больше,
     чем в предыдущем примере
    */
    for (var i = 0; i < maxIterations; i += 1) {
        console.log(i);
    }

    iteration ++;

    if (iteration < parts) {
    /*
     Так как число не маленькое(1000000), то таймер необходимо запускать с большим интервалом,
     в противном случае он не успев обработать одну часть(parts) до конца(от 1 до 2000)
     приступит к выполнению следующей 
    */
        setTimeout(createCycle, 1000); 
    }
}, 1000);

Если же вы разбили свой цикл на достаточно большое количество частей и в каждой части у вас вышло итераций немного, то смело ставьте интервал в 25мс(использование задержки менее 25мс в Internet Explorer может привести к блокированию браузера).

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

Обработка больших функций

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

function foo1() { console.log('foo1'); }
function foo2() { console.log('foo2'); }
function foo3() { console.log('foo3'); }

function createTasks() {
    var tasks = [foo1, foo2, foo3];

    setTimeout(function getTask() {
        tasks.shift()(); // вырезаем из массива первую функцию и обрабатываем ее, после чего берем следующую и т.д.

        if (tasks.length > 0) {
            setTimeout(getTask, 25);
        }
    }, 25);
}

createTasks();

В заключение


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

Автор: theNobody

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js