Используем время простоя веб-приложения для фоновых задач

в 15:39, , рубрики: javascript, производительность

Я люблю, когда мои приложения бегут со скоростью 60 fps, даже на мобильных устройствах. А еще я люблю сохранять состояние моего приложения, например, открытые окошки или введенный текст в localstorage и метаданных пользователя (если он зарегистрирован), чтобы, закрыв его, работу с ним можно было бы продолжить позже с того же места, в том числе и на другом устройстве.

Это все прекрасно, вот только сегодня я столкнулся с одной проблемой. Дело в том, что есть у меня одно боковое меню, offcanvas, и его состояние (открыто/закрыто) я тоже бы хотел сохранить в браузере и учетной записи пользователя. Вот только запись в localstorage и AJAX реквест на обновление в БД асинхронны и они все время норовят запускаться прямо во время сложной анимации, крадя у меня пару-другую фреймов, что особенно заметно на мобильных устройствах. Очевидно, мне бы хотелось, чтобы данные сохранялись после того, как анимация завершится, а не в критичный момент моего приложения, но как?

Добавление в вверх стека путем setTimeout(doPostponedStuff) не помогает, так как анимация сама асинхронна, и вызов doPostponedStuff перемешивается с вызовами фреймов анимации. Может захардкодить длительность анимации в качестве второго параметра setTimeout, то есть setTimeout(doPostponedStuff, 680)? Нет уж, увольте, я не хочу гореть в программистском аду. Подождите-ка, а ведь что-то это мне напоминает. Две ресурсоемкие задачи, из которых одна второстепенная, и она не должна мешать первостепенной… Ах да! Очень похоже на высчитывание координат элемента с position: absolute или position: fixed при изменении размеров окна браузера или прокрутке. Если подойти к этой задаче тривиально, у нас получится что-то вроде:

$(window).resize(function(){
    $target.css('top', x).css('left', y).css('width', z)
});

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

Дебаунсер

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

function debounce(ms, cb){
    var timeout = null;
    return function(){
        if(timeout) clearTimeout(timeout);
        timeout = setTimeout(cb, ms);
    }
}

и использовать его можно следующим образом:

$(window).scroll(debounce(100, repositionElement);

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

Дебаунсинг критического кода приложения

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

var saveAppStateWhenIdle = debounce(1000, saveAppState);

и в критических местах нашего приложения вызовем

saveAppStateWhenIdle ();

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

Дебаунсинг периодически повторяемого кода

В качестве другого примера, можно предположить, что у нас есть самый обыкновенный (микро)блог, в котором новые посты подгружаются AJAX-ом, да еще и красиво анимируются при этом, вот только если интервал обновления совпадет по времени с прокруткой, приложение немного подлагивает. Мелочь, а неприятно. Для решения данной проблемы можно написать следующее решение:

var loadPostsWhenIdle = debounce(1000, loadMorePosts);
setInterval(loadPostsWhenIdle, 10000);
$(window).scroll(loadPostsWhenIdle);

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

К сожалению, у этого метода есть один недостаток, а именно, loadPostsWhenIdle будет вызван после каждого завершения прокрутки, даже если между ними прошло менее 11 секунд, то есть принцип «не чаще, чем раз в 11 секунд», не соблюдается. Для решения этой проблемы, можно использовать булевый переключатель, в котором «истина» означает, что приложение занято, а «ложь», что оно простаивает, и можно выполнить второстепенную задачу:

var appIsBusy = false;

var onAppIsIdle = debounce(1000, function(){
    appIsBusy = false;
});

var doingPerformanceHeavyStuff = function(){
   appIsBusy = true;
   onAppIsIdle();
}

setInterval(function (){
   if(!appIsBusy) loadMorePosts();
}, 10000);

$window.scroll(doingPerformanceHeavyStuff);

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

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

Автор: vasilerusnac

Источник

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


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