Очередь асинхронных запросов на jQuery (ajax) или троттлинг

Очередь асинхронных запросов на jQuery (ajax) или троттлинг

Сегодня, асинхронные запросы, они же ajax с одноименной функцией в jQuery, используются чуть ли не на каждом сайте. Ведь, сама идея частичного изменения контента на странице обладает массой плюсов. Это и скорость, и объемы трафика, и удобство, и динамичность страниц. Однако, существует одна маленькая хитрость, внимание на которую редко где обращают. Хотя, со временем именно с ней в том или ином виде встречается каждый. Речь идет об организации очереди асинхронных запросов или троттлинге.

Зачем это нужно? - можете спросить вы. Все очень просто. Несмотря на то, что ajax обладает массой преимуществ, нельзя упускать из виду, что запрос к серверу остается запросом к серверу, со всем вытекающим отсюда.

Например, представьте себе следующую ситуацию. У вас есть обычный сайт на php, это может быть и другой язык - не особенно важно. Каждый запрос к серверу требует от него минимально выделить оперативную память на обработку запроса. Допустим, это будет 20 Мб. Загрузка же полной страницы сложного приложения потребует, к примеру, 50 Мб. Разбивая страницу на то, что стоит загружать пользователям сразу, а что стоит загружать асинхронно, вы можете создать ситуацию, когда ваше асинхронное приложение будет требовательнее и тяжелее того же приложения, но с классическим подходом. К примеру, вместо одного запроса на 50 Мб, вы можете создать, пусть даже и оптимизированных, десяток другой 20 Мб запросов в дополнение к первому запросу, которые суммарно будут существенно сильнее загружать сервер.

Проблема ajax-приложений 

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

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

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

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

И первым делом подкорректируем страницу index.html, оставив только несколько типовых функций, и слегка изменим структуру таблиц следующим образом:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
    <head>
        <meta http-equiv="content-type" content="text/html; charset=utf-8">
        <link href="https://code.jquery.com/ui/1.11.0/themes/ui-lightness/jquery-ui.css" rel="stylesheet" type="text/css"/>
        <link href="/css/template.css" rel="stylesheet" type="text/css"/>
 
        <script src="https://code.jquery.com/jquery-1.11.1.min.js"></script>
 
    </head>
 
    <script type=text/javascript>
        function clearTable() {
            $('.table-loader td').html('');
        }
        
        function setLoaderImgs(selector) {
            $(selector + ' .table-loader td').html('<img src="/images/ajax-loader.gif" />');
        }
        
        function getFakeLoader(startDate, selector, func) {
            return function () {
                func('data.json?callback=?', {    
                    contentType: "text/plain; charset=utf-8",
                    dataType: 'jsonp',
                    jsonpCallback: function (result) {
                        // Пишем время загрузки
                        $(selector).html('Время от начала: ' + ((+new Date() - startDate)/1000.0).toFixed(2) + ' секунд');
                    }
                });
            };
        }
        
        function formTestFor(selector, func)
        {
            $(selector + ' .start-process').click(function () {
                
            });
        }
        
        $(function () {
            formTestFor('.left-table');
            formTestFor('.right-table');
        });
    </script>
 
    <body>
        <h1>Очередь асинхронных запросов на jQuery (ajax) или троттлинг </h1>
        <hr/>
        <div class="left-table">
            <h3>Простой подход к загрузке</h3>
            <button class="start-process">Начать загрузку</button>
            <table class="table-loader">
                <tr class="row-1">
                    <td class="td"></td>
                </tr>
                <tr class="row-2">
                    <td class="td"></td>
                </tr>
                <tr class="row-3">
                    <td class="td"></td>
                </tr>
                <tr class="row-4">
                    <td class="td"></td>
                </tr>
                <tr class="row-5">
                    <td class="td"></td>
                </tr>
                <tr class="row-6">
                    <td class="td"></td>
                </tr>
                <tr class="row-7">
                    <td class="td"></td>
                </tr>
                <tr class="row-8">
                    <td class="td"></td>
                </tr>
            </table>
        </div>
        <div class="right-table">
            <h3>Более гибкий подход к загрузке</h3>
            <button class="start-process">Начать загрузку</button>
            <table class="table-loader">
                <tr class="row-1">
                    <td class="td"></td>
                </tr>
                <tr class="row-2">
                    <td class="td"></td>
                </tr>
                <tr class="row-3">
                    <td class="td"></td>
                </tr>
                <tr class="row-4">
                    <td class="td"></td>
                </tr>
                <tr class="row-5">
                    <td class="td"></td>
                </tr>
                <tr class="row-6">
                    <td class="td"></td>
                </tr>
                <tr class="row-7">
                    <td class="td"></td>
                </tr>
                <tr class="row-8">
                    <td class="td"></td>
                </tr>
            </table>
        </div>
    </body>
</html>

Заметьте, что функция getFakeLoader была немного переделана, чтобы для ajax-запросов вместо привычного вида функции $.ajax (jQuery.ajax) вызывалась функция из параметра.

Теперь можно приступать к самим способам.

 

Организация простой очереди асинхронных запросов на jQuery (троттлинг)

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

// Простая очередь
$.qajaxSimple = function (url, options) {
    // Параметр в миллисекундах, когда необходимо запускать следующий ajax запрос
    var intervalTimeout = 100;
    
    // Определяем переменную для хранения очереди
    $._qajaxSimpleQueue = $._qajaxSimpleQueue || [];
    
    // Сохраняем параметры запроса
    $._qajaxSimpleQueue.push({
        url: url,
        options: options
    });
    
    // Определяем циклический запуск асинхронного запроса по времени
    // Или если функция уже определена, то созерцаем красоту присваивания переменной самой себе
    $._qajaxSimpleInterval = $._qajaxSimpleInterval ||
        setInterval(function () {
            // Получаем первый запрос
            var params = $._qajaxSimpleQueue.shift();
            // Если данные в запросе хранились, то запускаем ajax-запрос
            if(params) {
                $.ajax(params.url, params.options);
            }
        }, intervalTimeout);
};
 

Так же изменим функцию formTestFor, чтобы наглядно увидеть результаты, и подкорректируем обработчик события загрузки DOM:

function formTestFor(selector, func)
{
    $(selector + ' .start-process').click(function () {
        getFakeLoader(new Date(), selector + ' .row-1 td', func)();
        getFakeLoader(new Date(), selector + ' .row-2 td', func)();
        getFakeLoader(new Date(), selector + ' .row-3 td', func)();
        getFakeLoader(new Date(), selector + ' .row-4 td', func)();
        getFakeLoader(new Date(), selector + ' .row-5 td', func)();
        getFakeLoader(new Date(), selector + ' .row-6 td', func)();
        getFakeLoader(new Date(), selector + ' .row-7 td', func)();
        getFakeLoader(new Date(), selector + ' .row-8 td', func)();
    });
}
 
$(function () {
    formTestFor('.left-table', $.qajaxSimple);
    formTestFor('.right-table');
});

Теперь, нажав на кнопку запуска процесса из левой таблицы, вы должны увидеть примерно следующее:

Пример простого троттлинга ajax на jQuery

Примечание: Интервал задержки составлял 100 мс.

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

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

Во-вторых, если вам нужно запустить 10 запросов, то до запуска 10-го запроса придется ждать примерно 1 секунду (при текущем параметре 100 мс) - это несколько расточительно, особенно, если изначальная страница генерируется намного быстрее. Учитывайте, что это тестовые запросы, которые выполняются практически моментально. Конечно, вы можете подобрать оптимальный для вас интервал ожидания и вызывать запросы в нужном вам порядке, но все же это не тот результат, которого бы хотелось достичь (дополнительная ненужная задержка).

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

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

 

Пакетная очередь аjax-запросов на jQuery с распределением по времени

Идея пакетной очередь ajax-запросов на jQuery с распределением по времени очень проста и совмещает в себе решение проблем предыдущего подхода. Суть заключается в следующем. Вы создаете специальный счетчик, который ограничивает количество разрешенных асинхронных запросов в интервале времени. И если этот счетчик превышается, то запрос откладывается до следующего интервала времени. Такой подход позволяет, во-первых, не ограничиваться интервалом ожидания (не ждать время пока запустится циклическая функция для запуска запроса), а во-вторых, уменьшить время простоя для запуска первых ajax-запросов. И вот код:

// Пакетная очередь аjax-запросов на jQuery с распределением по времени
$.qajax = function (url, options) {
    // Параметр в миллисекундах, когда необходимо запускать следующий ajax запрос
    var intervalTimeout = 550;
    // Количество разрешенных запросов в интервале времени.
    var countAjax = 5;
    
    // Определяем переменную для хранения очереди
    $._qajaxQueue = $._qajaxQueue || [];
    
    // Количество использованных запросов
    $._qajaxUsed = ($._qajaxUsed || 0) + 0;
    
    // Сохраняем параметры запроса
    $._qajaxQueue.push({
        url: url,
        options: options
    });
 
    // Если еще можно запускать запросы в интервале
    while ($._qajaxUsed < countAjax && $._qajaxQueue.length > 0) {
        // Получаем первый запрос
        var params = $._qajaxQueue.shift();
        // Если данные в запросе хранились, то запускаем ajax-запрос
        if(params) {
            $.ajax(params.url, params.options);
        }
        // Увеличиваем счетчик
        $._qajaxUsed++;
    }
    
    // Определяем циклический запуск асинхронного запроса по времени
    // Или если функция уже определена, то созерцаем красоту присваивания переменной самой себе
    $._qajaxInterval = $._qajaxInterval ||
        setInterval(function () {
            // Обнуляем счетчик использованных запросов
            $._qajaxUsed = 0;
            // Пробегаемся по сохраненным ранее запросам и запускаем первые countAjax запросов
            while ($._qajaxUsed < countAjax && $._qajaxQueue.length > 0) {
                // Получаем первый запрос
                var params = $._qajaxQueue.shift();
                // Если данные в запросе хранились, то запускаем ajax-запрос
                if(params) {
                    $.ajax(params.url, params.options);
                }
                // Увеличиваем счетчик
                $._qajaxUsed++;
            }
        }, intervalTimeout);
};

Так же подкорректируем обработчик события загрузки DOM: 

$(function () {
    formTestFor('.left-table', $.qajaxSimple);
    formTestFor('.right-table', $.qajax);
});

Теперь, нажав на кнопку запуска процесса из правой таблицы, вы должны увидеть примерно следующее:

Пакетная очередь аjax-запросов на jQuery с распределением по времени

Примечание: Интервал составлял 550 мс, количество разрешенных запросов 5.

Как можно увидеть, теперь, запустив одновременно 8 асинхронных запросов, они будут распределены во времени. Первые 5 запросов будут выполнены в то же время, как они были запущены, а оставшиеся 3 будут вызваны после 550 мс. Такой подход позволил сократить нагрузку и уменьшить общее ожидание.

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

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

Ссылка для скачивания проекта тут:

ajax_ochered.zip 

Социальные сети

☕ Понравился обзор? Поделитесь с друзьями!

Комментарии / отзывы  

0 # Сергей 12.05.2019 04:03
В следующий раз еще сложнее код пишите, чтобы разобраться было в разы труднее
Ответить | Ответить с цитатой | Цитировать | Сообщить модератору
0 # Игорь (Администратор) 14.05.2019 00:20
Вероятно, это вас расстроит, но это не сложный код.

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

П.С. Через некоторое время, если будете часто писать скрипты с jQuery, сами такие клацать будете.
Ответить | Ответить с цитатой | Цитировать | Сообщить модератору
Добавить комментарий / отзыв
Комментарий - это вежливое и наполненное смыслом сообщение (правила).



* Нажимая на кнопку "Отправить", Вы соглашаетесь с политикой конфиденциальности.
Социальные сети
Программы (Freeware, OpenSource...)